diff options
author | Chocobozzz <me@florianbigard.com> | 2019-05-16 16:55:34 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2019-05-16 17:16:47 +0200 |
commit | 536598cfafab1c5e24e881db1c528489f804fb6b (patch) | |
tree | 9d15e809218174622d68d21b0c7f0a73147187db | |
parent | 3daaa1927474869f8dbaddd6b94b4c071e314e10 (diff) | |
download | PeerTube-536598cfafab1c5e24e881db1c528489f804fb6b.tar.gz PeerTube-536598cfafab1c5e24e881db1c528489f804fb6b.tar.zst PeerTube-536598cfafab1c5e24e881db1c528489f804fb6b.zip |
Add audio support in upload
27 files changed, 324 insertions, 162 deletions
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html index 637484622..44fc6dc26 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html | |||
@@ -287,6 +287,14 @@ | |||
287 | </div> | 287 | </div> |
288 | 288 | ||
289 | <div class="form-group"> | 289 | <div class="form-group"> |
290 | <my-peertube-checkbox | ||
291 | inputName="transcodingAllowAudioFiles" formControlName="allowAudioFiles" | ||
292 | i18n-labelText labelText="Allow audio files upload" | ||
293 | i18n-helpHtml helpHtml="Allow your users to upload audio files that will be merged with the preview file on upload" | ||
294 | ></my-peertube-checkbox> | ||
295 | </div> | ||
296 | |||
297 | <div class="form-group"> | ||
290 | <label i18n for="transcodingThreads">Transcoding threads</label> | 298 | <label i18n for="transcodingThreads">Transcoding threads</label> |
291 | <div class="peertube-select-container"> | 299 | <div class="peertube-select-container"> |
292 | <select id="transcodingThreads" formControlName="threads"> | 300 | <select id="transcodingThreads" formControlName="threads"> |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts index e64750713..c238a6c81 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts | |||
@@ -116,6 +116,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
116 | enabled: null, | 116 | enabled: null, |
117 | threads: this.customConfigValidatorsService.TRANSCODING_THREADS, | 117 | threads: this.customConfigValidatorsService.TRANSCODING_THREADS, |
118 | allowAdditionalExtensions: null, | 118 | allowAdditionalExtensions: null, |
119 | allowAudioFiles: null, | ||
119 | resolutions: {} | 120 | resolutions: {} |
120 | }, | 121 | }, |
121 | autoBlacklist: { | 122 | autoBlacklist: { |
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts index b147b75b0..d8ba4df89 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts | |||
@@ -561,8 +561,12 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
561 | private flushPlayer () { | 561 | private flushPlayer () { |
562 | // Remove player if it exists | 562 | // Remove player if it exists |
563 | if (this.player) { | 563 | if (this.player) { |
564 | this.player.dispose() | 564 | try { |
565 | this.player = undefined | 565 | this.player.dispose() |
566 | this.player = undefined | ||
567 | } catch (err) { | ||
568 | console.error('Cannot dispose player.', err) | ||
569 | } | ||
566 | } | 570 | } |
567 | } | 571 | } |
568 | } | 572 | } |
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts index 6cdd54372..31cbc7dfd 100644 --- a/client/src/assets/player/peertube-player-manager.ts +++ b/client/src/assets/player/peertube-player-manager.ts | |||
@@ -117,8 +117,17 @@ export class PeertubePlayerManager { | |||
117 | videojs(options.common.playerElement, videojsOptions, function (this: any) { | 117 | videojs(options.common.playerElement, videojsOptions, function (this: any) { |
118 | const player = this | 118 | const player = this |
119 | 119 | ||
120 | player.tech_.one('error', () => self.maybeFallbackToWebTorrent(mode, player, options)) | 120 | let alreadyFallback = false |
121 | player.one('error', () => self.maybeFallbackToWebTorrent(mode, player, options)) | 121 | |
122 | player.tech_.one('error', () => { | ||
123 | if (!alreadyFallback) self.maybeFallbackToWebTorrent(mode, player, options) | ||
124 | alreadyFallback = true | ||
125 | }) | ||
126 | |||
127 | player.one('error', () => { | ||
128 | if (!alreadyFallback) self.maybeFallbackToWebTorrent(mode, player, options) | ||
129 | alreadyFallback = true | ||
130 | }) | ||
122 | 131 | ||
123 | self.addContextMenu(mode, player, options.common.embedUrl) | 132 | self.addContextMenu(mode, player, options.common.embedUrl) |
124 | 133 | ||
diff --git a/config/default.yaml b/config/default.yaml index 37ef4366f..9c9fd93dd 100644 --- a/config/default.yaml +++ b/config/default.yaml | |||
@@ -174,6 +174,8 @@ transcoding: | |||
174 | enabled: true | 174 | enabled: true |
175 | # Allow your users to upload .mkv, .mov, .avi, .flv videos | 175 | # Allow your users to upload .mkv, .mov, .avi, .flv videos |
176 | allow_additional_extensions: true | 176 | allow_additional_extensions: true |
177 | # If a user uploads an audio file, PeerTube will create a video by merging the preview file and the audio file | ||
178 | allow_audio_files: true | ||
177 | threads: 1 | 179 | threads: 1 |
178 | resolutions: # Only created if the original video has a higher resolution, uses more storage! | 180 | resolutions: # Only created if the original video has a higher resolution, uses more storage! |
179 | 240p: false | 181 | 240p: false |
diff --git a/config/production.yaml.example b/config/production.yaml.example index f84e15670..0ab99ac45 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example | |||
@@ -188,6 +188,8 @@ transcoding: | |||
188 | enabled: true | 188 | enabled: true |
189 | # Allow your users to upload .mkv, .mov, .avi, .flv videos | 189 | # Allow your users to upload .mkv, .mov, .avi, .flv videos |
190 | allow_additional_extensions: true | 190 | allow_additional_extensions: true |
191 | # If a user uploads an audio file, PeerTube will create a video by merging the preview file and the audio file | ||
192 | allow_audio_files: true | ||
191 | threads: 1 | 193 | threads: 1 |
192 | resolutions: # Only created if the original video has a higher resolution, uses more storage! | 194 | resolutions: # Only created if the original video has a higher resolution, uses more storage! |
193 | 240p: false | 195 | 240p: false |
diff --git a/config/test.yaml b/config/test.yaml index 682530840..7dabe433c 100644 --- a/config/test.yaml +++ b/config/test.yaml | |||
@@ -55,6 +55,7 @@ signup: | |||
55 | transcoding: | 55 | transcoding: |
56 | enabled: true | 56 | enabled: true |
57 | allow_additional_extensions: false | 57 | allow_additional_extensions: false |
58 | allow_audio_files: false | ||
58 | threads: 2 | 59 | threads: 2 |
59 | resolutions: | 60 | resolutions: |
60 | 240p: true | 61 | 240p: true |
diff --git a/scripts/create-transcoding-job.ts b/scripts/create-transcoding-job.ts index 4a677eacb..2b7cb5177 100755 --- a/scripts/create-transcoding-job.ts +++ b/scripts/create-transcoding-job.ts | |||
@@ -2,6 +2,7 @@ import * as program from 'commander' | |||
2 | import { VideoModel } from '../server/models/video/video' | 2 | import { VideoModel } from '../server/models/video/video' |
3 | import { initDatabaseModels } from '../server/initializers' | 3 | import { initDatabaseModels } from '../server/initializers' |
4 | import { JobQueue } from '../server/lib/job-queue' | 4 | import { JobQueue } from '../server/lib/job-queue' |
5 | import { VideoTranscodingPayload } from '../server/lib/job-queue/handlers/video-transcoding' | ||
5 | 6 | ||
6 | program | 7 | program |
7 | .option('-v, --video [videoUUID]', 'Video UUID') | 8 | .option('-v, --video [videoUUID]', 'Video UUID') |
@@ -31,15 +32,9 @@ async function run () { | |||
31 | const video = await VideoModel.loadByUUIDWithFile(program['video']) | 32 | const video = await VideoModel.loadByUUIDWithFile(program['video']) |
32 | if (!video) throw new Error('Video not found.') | 33 | if (!video) throw new Error('Video not found.') |
33 | 34 | ||
34 | const dataInput = { | 35 | const dataInput: VideoTranscodingPayload = program.resolution !== undefined |
35 | videoUUID: video.uuid, | 36 | ? { type: 'new-resolution' as 'new-resolution', videoUUID: video.uuid, isNewVideo: false, resolution: program.resolution } |
36 | isNewVideo: false, | 37 | : { type: 'optimize' as 'optimize', videoUUID: video.uuid, isNewVideo: false } |
37 | resolution: undefined | ||
38 | } | ||
39 | |||
40 | if (program.resolution !== undefined) { | ||
41 | dataInput.resolution = program.resolution | ||
42 | } | ||
43 | 38 | ||
44 | await JobQueue.Instance.init() | 39 | await JobQueue.Instance.init() |
45 | await JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput }) | 40 | await JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput }) |
diff --git a/server/assets/default-audio-background.jpg b/server/assets/default-audio-background.jpg new file mode 100644 index 000000000..a19173eac --- /dev/null +++ b/server/assets/default-audio-background.jpg | |||
Binary files differ | |||
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index 40012c03b..d9ce6a153 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts | |||
@@ -255,6 +255,7 @@ function customConfig (): CustomConfig { | |||
255 | transcoding: { | 255 | transcoding: { |
256 | enabled: CONFIG.TRANSCODING.ENABLED, | 256 | enabled: CONFIG.TRANSCODING.ENABLED, |
257 | allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS, | 257 | allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS, |
258 | allowAudioFiles: CONFIG.TRANSCODING.ALLOW_AUDIO_FILES, | ||
258 | threads: CONFIG.TRANSCODING.THREADS, | 259 | threads: CONFIG.TRANSCODING.THREADS, |
259 | resolutions: { | 260 | resolutions: { |
260 | '240p': CONFIG.TRANSCODING.RESOLUTIONS[ '240p' ], | 261 | '240p': CONFIG.TRANSCODING.RESOLUTIONS[ '240p' ], |
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 1a18a8ae8..a2a615a79 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -1,12 +1,12 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { extname, join } from 'path' | 2 | import { extname, join } from 'path' |
3 | import { VideoCreate, VideoPrivacy, VideoState, VideoUpdate } from '../../../../shared' | 3 | import { VideoCreate, VideoPrivacy, VideoResolution, VideoState, VideoUpdate } from '../../../../shared' |
4 | import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' | 4 | import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' |
5 | import { logger } from '../../../helpers/logger' | 5 | import { logger } from '../../../helpers/logger' |
6 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | 6 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' |
7 | import { getFormattedObjects, getServerActor } from '../../../helpers/utils' | 7 | import { getFormattedObjects, getServerActor } from '../../../helpers/utils' |
8 | import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' | 8 | import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' |
9 | import { MIMETYPES, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers/constants' | 9 | import { MIMETYPES, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES, DEFAULT_AUDIO_RESOLUTION } from '../../../initializers/constants' |
10 | import { | 10 | import { |
11 | changeVideoChannelShare, | 11 | changeVideoChannelShare, |
12 | federateVideoIfNeeded, | 12 | federateVideoIfNeeded, |
@@ -54,6 +54,7 @@ import { CONFIG } from '../../../initializers/config' | |||
54 | import { sequelizeTypescript } from '../../../initializers/database' | 54 | import { sequelizeTypescript } from '../../../initializers/database' |
55 | import { createVideoMiniatureFromExisting, generateVideoMiniature } from '../../../lib/thumbnail' | 55 | import { createVideoMiniatureFromExisting, generateVideoMiniature } from '../../../lib/thumbnail' |
56 | import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' | 56 | import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' |
57 | import { VideoTranscodingPayload } from '../../../lib/job-queue/handlers/video-transcoding' | ||
57 | 58 | ||
58 | const auditLogger = auditLoggerFactory('videos') | 59 | const auditLogger = auditLoggerFactory('videos') |
59 | const videosRouter = express.Router() | 60 | const videosRouter = express.Router() |
@@ -191,18 +192,19 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
191 | const video = new VideoModel(videoData) | 192 | const video = new VideoModel(videoData) |
192 | video.url = getVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object | 193 | video.url = getVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object |
193 | 194 | ||
194 | // Build the file object | ||
195 | const { videoFileResolution } = await getVideoFileResolution(videoPhysicalFile.path) | ||
196 | const fps = await getVideoFileFPS(videoPhysicalFile.path) | ||
197 | |||
198 | const videoFileData = { | 195 | const videoFileData = { |
199 | extname: extname(videoPhysicalFile.filename), | 196 | extname: extname(videoPhysicalFile.filename), |
200 | resolution: videoFileResolution, | 197 | size: videoPhysicalFile.size |
201 | size: videoPhysicalFile.size, | ||
202 | fps | ||
203 | } | 198 | } |
204 | const videoFile = new VideoFileModel(videoFileData) | 199 | const videoFile = new VideoFileModel(videoFileData) |
205 | 200 | ||
201 | if (!videoFile.isAudio()) { | ||
202 | videoFile.fps = await getVideoFileFPS(videoPhysicalFile.path) | ||
203 | videoFile.resolution = (await getVideoFileResolution(videoPhysicalFile.path)).videoFileResolution | ||
204 | } else { | ||
205 | videoFile.resolution = DEFAULT_AUDIO_RESOLUTION | ||
206 | } | ||
207 | |||
206 | // Move physical file | 208 | // Move physical file |
207 | const videoDir = CONFIG.STORAGE.VIDEOS_DIR | 209 | const videoDir = CONFIG.STORAGE.VIDEOS_DIR |
208 | const destination = join(videoDir, video.getVideoFilename(videoFile)) | 210 | const destination = join(videoDir, video.getVideoFilename(videoFile)) |
@@ -279,9 +281,21 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
279 | 281 | ||
280 | if (video.state === VideoState.TO_TRANSCODE) { | 282 | if (video.state === VideoState.TO_TRANSCODE) { |
281 | // Put uuid because we don't have id auto incremented for now | 283 | // Put uuid because we don't have id auto incremented for now |
282 | const dataInput = { | 284 | let dataInput: VideoTranscodingPayload |
283 | videoUUID: videoCreated.uuid, | 285 | |
284 | isNewVideo: true | 286 | if (videoFile.isAudio()) { |
287 | dataInput = { | ||
288 | type: 'merge-audio' as 'merge-audio', | ||
289 | resolution: DEFAULT_AUDIO_RESOLUTION, | ||
290 | videoUUID: videoCreated.uuid, | ||
291 | isNewVideo: true | ||
292 | } | ||
293 | } else { | ||
294 | dataInput = { | ||
295 | type: 'optimize' as 'optimize', | ||
296 | videoUUID: videoCreated.uuid, | ||
297 | isNewVideo: true | ||
298 | } | ||
285 | } | 299 | } |
286 | 300 | ||
287 | await JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput }) | 301 | await JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput }) |
diff --git a/server/controllers/static.ts b/server/controllers/static.ts index 05019fcc2..d57dba6ce 100644 --- a/server/controllers/static.ts +++ b/server/controllers/static.ts | |||
@@ -181,7 +181,7 @@ async function getVideoCaption (req: express.Request, res: express.Response) { | |||
181 | return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE }) | 181 | return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE }) |
182 | } | 182 | } |
183 | 183 | ||
184 | async function generateNodeinfo (req: express.Request, res: express.Response, next: express.NextFunction) { | 184 | async function generateNodeinfo (req: express.Request, res: express.Response) { |
185 | const { totalVideos } = await VideoModel.getStats() | 185 | const { totalVideos } = await VideoModel.getStats() |
186 | const { totalLocalVideoComments } = await VideoCommentModel.getStats() | 186 | const { totalLocalVideoComments } = await VideoCommentModel.getStats() |
187 | const { totalUsers } = await UserModel.getStats() | 187 | const { totalUsers } = await UserModel.getStats() |
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 2fdf34cb7..c180da832 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -117,37 +117,50 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima | |||
117 | } | 117 | } |
118 | } | 118 | } |
119 | 119 | ||
120 | type TranscodeOptions = { | 120 | type TranscodeOptionsType = 'hls' | 'quick-transcode' | 'video' | 'merge-audio' |
121 | |||
122 | interface BaseTranscodeOptions { | ||
123 | type: TranscodeOptionsType | ||
121 | inputPath: string | 124 | inputPath: string |
122 | outputPath: string | 125 | outputPath: string |
123 | resolution: VideoResolution | 126 | resolution: VideoResolution |
124 | isPortraitMode?: boolean | 127 | isPortraitMode?: boolean |
125 | doQuickTranscode?: Boolean | 128 | } |
126 | 129 | ||
127 | hlsPlaylist?: { | 130 | interface HLSTranscodeOptions extends BaseTranscodeOptions { |
131 | type: 'hls' | ||
132 | hlsPlaylist: { | ||
128 | videoFilename: string | 133 | videoFilename: string |
129 | } | 134 | } |
130 | } | 135 | } |
131 | 136 | ||
137 | interface QuickTranscodeOptions extends BaseTranscodeOptions { | ||
138 | type: 'quick-transcode' | ||
139 | } | ||
140 | |||
141 | interface VideoTranscodeOptions extends BaseTranscodeOptions { | ||
142 | type: 'video' | ||
143 | } | ||
144 | |||
145 | interface MergeAudioTranscodeOptions extends BaseTranscodeOptions { | ||
146 | type: 'merge-audio' | ||
147 | audioPath: string | ||
148 | } | ||
149 | |||
150 | type TranscodeOptions = HLSTranscodeOptions | VideoTranscodeOptions | MergeAudioTranscodeOptions | QuickTranscodeOptions | ||
151 | |||
132 | function transcode (options: TranscodeOptions) { | 152 | function transcode (options: TranscodeOptions) { |
133 | return new Promise<void>(async (res, rej) => { | 153 | return new Promise<void>(async (res, rej) => { |
134 | try { | 154 | try { |
135 | let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING }) | 155 | let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING }) |
136 | .output(options.outputPath) | 156 | .output(options.outputPath) |
137 | 157 | ||
138 | if (options.doQuickTranscode) { | 158 | if (options.type === 'quick-transcode') { |
139 | if (options.hlsPlaylist) { | 159 | command = await buildQuickTranscodeCommand(command) |
140 | throw(Error("Quick transcode and HLS can't be used at the same time")) | 160 | } else if (options.type === 'hls') { |
141 | } | ||
142 | |||
143 | command | ||
144 | .format('mp4') | ||
145 | .addOption('-c:v copy') | ||
146 | .addOption('-c:a copy') | ||
147 | .outputOption('-map_metadata -1') // strip all metadata | ||
148 | .outputOption('-movflags faststart') | ||
149 | } else if (options.hlsPlaylist) { | ||
150 | command = await buildHLSCommand(command, options) | 161 | command = await buildHLSCommand(command, options) |
162 | } else if (options.type === 'merge-audio') { | ||
163 | command = await buildAudioMergeCommand(command, options) | ||
151 | } else { | 164 | } else { |
152 | command = await buildx264Command(command, options) | 165 | command = await buildx264Command(command, options) |
153 | } | 166 | } |
@@ -163,7 +176,7 @@ function transcode (options: TranscodeOptions) { | |||
163 | return rej(err) | 176 | return rej(err) |
164 | }) | 177 | }) |
165 | .on('end', () => { | 178 | .on('end', () => { |
166 | return onTranscodingSuccess(options) | 179 | return fixHLSPlaylistIfNeeded(options) |
167 | .then(() => res()) | 180 | .then(() => res()) |
168 | .catch(err => rej(err)) | 181 | .catch(err => rej(err)) |
169 | }) | 182 | }) |
@@ -205,6 +218,8 @@ export { | |||
205 | getVideoFileResolution, | 218 | getVideoFileResolution, |
206 | getDurationFromVideoFile, | 219 | getDurationFromVideoFile, |
207 | generateImageFromVideoFile, | 220 | generateImageFromVideoFile, |
221 | TranscodeOptions, | ||
222 | TranscodeOptionsType, | ||
208 | transcode, | 223 | transcode, |
209 | getVideoFileFPS, | 224 | getVideoFileFPS, |
210 | computeResolutionsToTranscode, | 225 | computeResolutionsToTranscode, |
@@ -215,7 +230,7 @@ export { | |||
215 | 230 | ||
216 | // --------------------------------------------------------------------------- | 231 | // --------------------------------------------------------------------------- |
217 | 232 | ||
218 | async function buildx264Command (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) { | 233 | async function buildx264Command (command: ffmpeg.FfmpegCommand, options: VideoTranscodeOptions) { |
219 | let fps = await getVideoFileFPS(options.inputPath) | 234 | let fps = await getVideoFileFPS(options.inputPath) |
220 | // On small/medium resolutions, limit FPS | 235 | // On small/medium resolutions, limit FPS |
221 | if ( | 236 | if ( |
@@ -226,7 +241,7 @@ async function buildx264Command (command: ffmpeg.FfmpegCommand, options: Transco | |||
226 | fps = VIDEO_TRANSCODING_FPS.AVERAGE | 241 | fps = VIDEO_TRANSCODING_FPS.AVERAGE |
227 | } | 242 | } |
228 | 243 | ||
229 | command = await presetH264(command, options.resolution, fps) | 244 | command = await presetH264(command, options.inputPath, options.resolution, fps) |
230 | 245 | ||
231 | if (options.resolution !== undefined) { | 246 | if (options.resolution !== undefined) { |
232 | // '?x720' or '720x?' for example | 247 | // '?x720' or '720x?' for example |
@@ -245,7 +260,29 @@ async function buildx264Command (command: ffmpeg.FfmpegCommand, options: Transco | |||
245 | return command | 260 | return command |
246 | } | 261 | } |
247 | 262 | ||
248 | async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) { | 263 | async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: MergeAudioTranscodeOptions) { |
264 | command = command.loop(undefined) | ||
265 | |||
266 | command = await presetH264VeryFast(command, options.audioPath, options.resolution) | ||
267 | |||
268 | command = command.input(options.audioPath) | ||
269 | .videoFilter('scale=trunc(iw/2)*2:trunc(ih/2)*2') // Avoid "height not divisible by 2" error | ||
270 | .outputOption('-tune stillimage') | ||
271 | .outputOption('-shortest') | ||
272 | |||
273 | return command | ||
274 | } | ||
275 | |||
276 | async function buildQuickTranscodeCommand (command: ffmpeg.FfmpegCommand) { | ||
277 | command = await presetCopy(command) | ||
278 | |||
279 | command = command.outputOption('-map_metadata -1') // strip all metadata | ||
280 | .outputOption('-movflags faststart') | ||
281 | |||
282 | return command | ||
283 | } | ||
284 | |||
285 | async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: HLSTranscodeOptions) { | ||
249 | const videoPath = getHLSVideoPath(options) | 286 | const videoPath = getHLSVideoPath(options) |
250 | 287 | ||
251 | command = await presetCopy(command) | 288 | command = await presetCopy(command) |
@@ -261,19 +298,19 @@ async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: Transcod | |||
261 | return command | 298 | return command |
262 | } | 299 | } |
263 | 300 | ||
264 | function getHLSVideoPath (options: TranscodeOptions) { | 301 | function getHLSVideoPath (options: HLSTranscodeOptions) { |
265 | return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}` | 302 | return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}` |
266 | } | 303 | } |
267 | 304 | ||
268 | async function onTranscodingSuccess (options: TranscodeOptions) { | 305 | async function fixHLSPlaylistIfNeeded (options: TranscodeOptions) { |
269 | if (!options.hlsPlaylist) return | 306 | if (options.type !== 'hls') return |
270 | 307 | ||
271 | // Fix wrong mapping with some ffmpeg versions | ||
272 | const fileContent = await readFile(options.outputPath) | 308 | const fileContent = await readFile(options.outputPath) |
273 | 309 | ||
274 | const videoFileName = options.hlsPlaylist.videoFilename | 310 | const videoFileName = options.hlsPlaylist.videoFilename |
275 | const videoFilePath = getHLSVideoPath(options) | 311 | const videoFilePath = getHLSVideoPath(options) |
276 | 312 | ||
313 | // Fix wrong mapping with some ffmpeg versions | ||
277 | const newContent = fileContent.toString() | 314 | const newContent = fileContent.toString() |
278 | .replace(`#EXT-X-MAP:URI="${videoFilePath}",`, `#EXT-X-MAP:URI="${videoFileName}",`) | 315 | .replace(`#EXT-X-MAP:URI="${videoFilePath}",`, `#EXT-X-MAP:URI="${videoFileName}",`) |
279 | 316 | ||
@@ -300,44 +337,27 @@ function getVideoStreamFromFile (path: string) { | |||
300 | * and quality. Superfast and ultrafast will give you better | 337 | * and quality. Superfast and ultrafast will give you better |
301 | * performance, but then quality is noticeably worse. | 338 | * performance, but then quality is noticeably worse. |
302 | */ | 339 | */ |
303 | async function presetH264VeryFast (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise<ffmpeg.FfmpegCommand> { | 340 | async function presetH264VeryFast (command: ffmpeg.FfmpegCommand, input: string, resolution: VideoResolution, fps?: number) { |
304 | let localCommand = await presetH264(command, resolution, fps) | 341 | let localCommand = await presetH264(command, input, resolution, fps) |
342 | |||
305 | localCommand = localCommand.outputOption('-preset:v veryfast') | 343 | localCommand = localCommand.outputOption('-preset:v veryfast') |
306 | .outputOption([ '--aq-mode=2', '--aq-strength=1.3' ]) | 344 | |
307 | /* | 345 | /* |
308 | MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html | 346 | MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html |
309 | Our target situation is closer to a livestream than a stream, | 347 | Our target situation is closer to a livestream than a stream, |
310 | since we want to reduce as much a possible the encoding burden, | 348 | since we want to reduce as much a possible the encoding burden, |
311 | altough not to the point of a livestream where there is a hard | 349 | although not to the point of a livestream where there is a hard |
312 | constraint on the frames per second to be encoded. | 350 | constraint on the frames per second to be encoded. |
313 | |||
314 | why '--aq-mode=2 --aq-strength=1.3' instead of '-profile:v main'? | ||
315 | Make up for most of the loss of grain and macroblocking | ||
316 | with less computing power. | ||
317 | */ | 351 | */ |
318 | 352 | ||
319 | return localCommand | 353 | return localCommand |
320 | } | 354 | } |
321 | 355 | ||
322 | /** | 356 | /** |
323 | * A preset optimised for a stillimage audio video | ||
324 | */ | ||
325 | async function presetStillImageWithAudio ( | ||
326 | command: ffmpeg.FfmpegCommand, | ||
327 | resolution: VideoResolution, | ||
328 | fps: number | ||
329 | ): Promise<ffmpeg.FfmpegCommand> { | ||
330 | let localCommand = await presetH264VeryFast(command, resolution, fps) | ||
331 | localCommand = localCommand.outputOption('-tune stillimage') | ||
332 | |||
333 | return localCommand | ||
334 | } | ||
335 | |||
336 | /** | ||
337 | * A toolbox to play with audio | 357 | * A toolbox to play with audio |
338 | */ | 358 | */ |
339 | namespace audio { | 359 | namespace audio { |
340 | export const get = (option: ffmpeg.FfmpegCommand | string) => { | 360 | export const get = (option: string) => { |
341 | // without position, ffprobe considers the last input only | 361 | // without position, ffprobe considers the last input only |
342 | // we make it consider the first input only | 362 | // we make it consider the first input only |
343 | // if you pass a file path to pos, then ffprobe acts on that file directly | 363 | // if you pass a file path to pos, then ffprobe acts on that file directly |
@@ -359,11 +379,7 @@ namespace audio { | |||
359 | return res({ absolutePath: data.format.filename }) | 379 | return res({ absolutePath: data.format.filename }) |
360 | } | 380 | } |
361 | 381 | ||
362 | if (typeof option === 'string') { | 382 | return ffmpeg.ffprobe(option, parseFfprobe) |
363 | return ffmpeg.ffprobe(option, parseFfprobe) | ||
364 | } | ||
365 | |||
366 | return option.ffprobe(parseFfprobe) | ||
367 | }) | 383 | }) |
368 | } | 384 | } |
369 | 385 | ||
@@ -405,7 +421,7 @@ namespace audio { | |||
405 | * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel | 421 | * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel |
406 | * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr | 422 | * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr |
407 | */ | 423 | */ |
408 | async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise<ffmpeg.FfmpegCommand> { | 424 | async function presetH264 (command: ffmpeg.FfmpegCommand, input: string, resolution: VideoResolution, fps?: number) { |
409 | let localCommand = command | 425 | let localCommand = command |
410 | .format('mp4') | 426 | .format('mp4') |
411 | .videoCodec('libx264') | 427 | .videoCodec('libx264') |
@@ -416,7 +432,7 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResol | |||
416 | .outputOption('-map_metadata -1') // strip all metadata | 432 | .outputOption('-map_metadata -1') // strip all metadata |
417 | .outputOption('-movflags faststart') | 433 | .outputOption('-movflags faststart') |
418 | 434 | ||
419 | const parsedAudio = await audio.get(localCommand) | 435 | const parsedAudio = await audio.get(input) |
420 | 436 | ||
421 | if (!parsedAudio.audioStream) { | 437 | if (!parsedAudio.audioStream) { |
422 | localCommand = localCommand.noAudio() | 438 | localCommand = localCommand.noAudio() |
@@ -425,28 +441,30 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResol | |||
425 | .audioCodec('libfdk_aac') | 441 | .audioCodec('libfdk_aac') |
426 | .audioQuality(5) | 442 | .audioQuality(5) |
427 | } else { | 443 | } else { |
428 | // we try to reduce the ceiling bitrate by making rough correspondances of bitrates | 444 | // we try to reduce the ceiling bitrate by making rough matches of bitrates |
429 | // of course this is far from perfect, but it might save some space in the end | 445 | // of course this is far from perfect, but it might save some space in the end |
446 | localCommand = localCommand.audioCodec('aac') | ||
447 | |||
430 | const audioCodecName = parsedAudio.audioStream[ 'codec_name' ] | 448 | const audioCodecName = parsedAudio.audioStream[ 'codec_name' ] |
431 | let bitrate: number | ||
432 | if (audio.bitrate[ audioCodecName ]) { | ||
433 | localCommand = localCommand.audioCodec('aac') | ||
434 | 449 | ||
435 | bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ]) | 450 | if (audio.bitrate[ audioCodecName ]) { |
451 | const bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ]) | ||
436 | if (bitrate !== undefined && bitrate !== -1) localCommand = localCommand.audioBitrate(bitrate) | 452 | if (bitrate !== undefined && bitrate !== -1) localCommand = localCommand.audioBitrate(bitrate) |
437 | } | 453 | } |
438 | } | 454 | } |
439 | 455 | ||
440 | // Constrained Encoding (VBV) | 456 | if (fps) { |
441 | // https://slhck.info/video/2017/03/01/rate-control.html | 457 | // Constrained Encoding (VBV) |
442 | // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate | 458 | // https://slhck.info/video/2017/03/01/rate-control.html |
443 | const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS) | 459 | // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate |
444 | localCommand = localCommand.outputOptions([`-maxrate ${ targetBitrate }`, `-bufsize ${ targetBitrate * 2 }`]) | 460 | const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS) |
445 | 461 | localCommand = localCommand.outputOptions([ `-maxrate ${targetBitrate}`, `-bufsize ${targetBitrate * 2}` ]) | |
446 | // Keyframe interval of 2 seconds for faster seeking and resolution switching. | 462 | |
447 | // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html | 463 | // Keyframe interval of 2 seconds for faster seeking and resolution switching. |
448 | // https://superuser.com/a/908325 | 464 | // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html |
449 | localCommand = localCommand.outputOption(`-g ${ fps * 2 }`) | 465 | // https://superuser.com/a/908325 |
466 | localCommand = localCommand.outputOption(`-g ${fps * 2}`) | ||
467 | } | ||
450 | 468 | ||
451 | return localCommand | 469 | return localCommand |
452 | } | 470 | } |
diff --git a/server/initializers/config.ts b/server/initializers/config.ts index 4f77e144d..4515bc804 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts | |||
@@ -140,6 +140,7 @@ const CONFIG = { | |||
140 | TRANSCODING: { | 140 | TRANSCODING: { |
141 | get ENABLED () { return config.get<boolean>('transcoding.enabled') }, | 141 | get ENABLED () { return config.get<boolean>('transcoding.enabled') }, |
142 | get ALLOW_ADDITIONAL_EXTENSIONS () { return config.get<boolean>('transcoding.allow_additional_extensions') }, | 142 | get ALLOW_ADDITIONAL_EXTENSIONS () { return config.get<boolean>('transcoding.allow_additional_extensions') }, |
143 | get ALLOW_AUDIO_FILES () { return config.get<boolean>('transcoding.allow_audio_files') }, | ||
143 | get THREADS () { return config.get<number>('transcoding.threads') }, | 144 | get THREADS () { return config.get<number>('transcoding.threads') }, |
144 | RESOLUTIONS: { | 145 | RESOLUTIONS: { |
145 | get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') }, | 146 | get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') }, |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 62778ae58..718d0893b 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -1,10 +1,10 @@ | |||
1 | import { join } from 'path' | 1 | import { join } from 'path' |
2 | import { JobType, VideoRateType, VideoState } from '../../shared/models' | 2 | import { JobType, VideoRateType, VideoResolution, VideoState } from '../../shared/models' |
3 | import { ActivityPubActorType } from '../../shared/models/activitypub' | 3 | import { ActivityPubActorType } from '../../shared/models/activitypub' |
4 | import { FollowState } from '../../shared/models/actors' | 4 | import { FollowState } from '../../shared/models/actors' |
5 | import { VideoAbuseState, VideoImportState, VideoPrivacy, VideoTranscodingFPS } from '../../shared/models/videos' | 5 | import { VideoAbuseState, VideoImportState, VideoPrivacy, VideoTranscodingFPS } from '../../shared/models/videos' |
6 | // Do not use barrels, remain constants as independent as possible | 6 | // Do not use barrels, remain constants as independent as possible |
7 | import { isTestInstance, sanitizeHost, sanitizeUrl } from '../helpers/core-utils' | 7 | import { isTestInstance, sanitizeHost, sanitizeUrl, root } from '../helpers/core-utils' |
8 | import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' | 8 | import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' |
9 | import { invert } from 'lodash' | 9 | import { invert } from 'lodash' |
10 | import { CronRepeatOptions, EveryRepeatOptions } from 'bull' | 10 | import { CronRepeatOptions, EveryRepeatOptions } from 'bull' |
@@ -228,7 +228,7 @@ let CONSTRAINTS_FIELDS = { | |||
228 | max: 2 * 1024 * 1024 // 2MB | 228 | max: 2 * 1024 * 1024 // 2MB |
229 | } | 229 | } |
230 | }, | 230 | }, |
231 | EXTNAME: buildVideosExtname(), | 231 | EXTNAME: [] as string[], |
232 | INFO_HASH: { min: 40, max: 40 }, // Length, info hash is 20 bytes length but we represent it in hexadecimal so 20 * 2 | 232 | INFO_HASH: { min: 40, max: 40 }, // Length, info hash is 20 bytes length but we represent it in hexadecimal so 20 * 2 |
233 | DURATION: { min: 0 }, // Number | 233 | DURATION: { min: 0 }, // Number |
234 | TAGS: { min: 0, max: 5 }, // Number of total tags | 234 | TAGS: { min: 0, max: 5 }, // Number of total tags |
@@ -300,6 +300,8 @@ const VIDEO_TRANSCODING_FPS: VideoTranscodingFPS = { | |||
300 | KEEP_ORIGIN_FPS_RESOLUTION_MIN: 720 // We keep the original FPS on high resolutions (720 minimum) | 300 | KEEP_ORIGIN_FPS_RESOLUTION_MIN: 720 // We keep the original FPS on high resolutions (720 minimum) |
301 | } | 301 | } |
302 | 302 | ||
303 | const DEFAULT_AUDIO_RESOLUTION = VideoResolution.H_480P | ||
304 | |||
303 | const VIDEO_RATE_TYPES: { [ id: string ]: VideoRateType } = { | 305 | const VIDEO_RATE_TYPES: { [ id: string ]: VideoRateType } = { |
304 | LIKE: 'like', | 306 | LIKE: 'like', |
305 | DISLIKE: 'dislike' | 307 | DISLIKE: 'dislike' |
@@ -380,8 +382,18 @@ const VIDEO_PLAYLIST_TYPES = { | |||
380 | } | 382 | } |
381 | 383 | ||
382 | const MIMETYPES = { | 384 | const MIMETYPES = { |
385 | AUDIO: { | ||
386 | MIMETYPE_EXT: { | ||
387 | 'audio/mpeg': '.mp3', | ||
388 | 'audio/mp3': '.mp3', | ||
389 | 'application/ogg': '.ogg', | ||
390 | 'audio/ogg': '.ogg', | ||
391 | 'audio/flac': '.flac' | ||
392 | }, | ||
393 | EXT_MIMETYPE: null as { [ id: string ]: string } | ||
394 | }, | ||
383 | VIDEO: { | 395 | VIDEO: { |
384 | MIMETYPE_EXT: buildVideoMimetypeExt(), | 396 | MIMETYPE_EXT: null as { [ id: string ]: string }, |
385 | EXT_MIMETYPE: null as { [ id: string ]: string } | 397 | EXT_MIMETYPE: null as { [ id: string ]: string } |
386 | }, | 398 | }, |
387 | IMAGE: { | 399 | IMAGE: { |
@@ -403,7 +415,7 @@ const MIMETYPES = { | |||
403 | } | 415 | } |
404 | } | 416 | } |
405 | } | 417 | } |
406 | MIMETYPES.VIDEO.EXT_MIMETYPE = invert(MIMETYPES.VIDEO.MIMETYPE_EXT) | 418 | MIMETYPES.AUDIO.EXT_MIMETYPE = invert(MIMETYPES.AUDIO.MIMETYPE_EXT) |
407 | 419 | ||
408 | // --------------------------------------------------------------------------- | 420 | // --------------------------------------------------------------------------- |
409 | 421 | ||
@@ -429,7 +441,7 @@ const ACTIVITY_PUB = { | |||
429 | COLLECTION_ITEMS_PER_PAGE: 10, | 441 | COLLECTION_ITEMS_PER_PAGE: 10, |
430 | FETCH_PAGE_LIMIT: 100, | 442 | FETCH_PAGE_LIMIT: 100, |
431 | URL_MIME_TYPES: { | 443 | URL_MIME_TYPES: { |
432 | VIDEO: Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT), | 444 | VIDEO: [] as string[], |
433 | TORRENT: [ 'application/x-bittorrent' ], | 445 | TORRENT: [ 'application/x-bittorrent' ], |
434 | MAGNET: [ 'application/x-bittorrent;x-scheme-handler/magnet' ] | 446 | MAGNET: [ 'application/x-bittorrent;x-scheme-handler/magnet' ] |
435 | }, | 447 | }, |
@@ -543,6 +555,10 @@ const REDUNDANCY = { | |||
543 | 555 | ||
544 | const ACCEPT_HEADERS = [ 'html', 'application/json' ].concat(ACTIVITY_PUB.POTENTIAL_ACCEPT_HEADERS) | 556 | const ACCEPT_HEADERS = [ 'html', 'application/json' ].concat(ACTIVITY_PUB.POTENTIAL_ACCEPT_HEADERS) |
545 | 557 | ||
558 | const ASSETS_PATH = { | ||
559 | DEFAULT_AUDIO_BACKGROUND: join(root(), 'server', 'assets', 'default-audio-background.jpg') | ||
560 | } | ||
561 | |||
546 | // --------------------------------------------------------------------------- | 562 | // --------------------------------------------------------------------------- |
547 | 563 | ||
548 | const CUSTOM_HTML_TAG_COMMENTS = { | 564 | const CUSTOM_HTML_TAG_COMMENTS = { |
@@ -612,6 +628,7 @@ if (isTestInstance() === true) { | |||
612 | } | 628 | } |
613 | 629 | ||
614 | updateWebserverUrls() | 630 | updateWebserverUrls() |
631 | updateWebserverConfig() | ||
615 | 632 | ||
616 | registerConfigChangedHandler(() => { | 633 | registerConfigChangedHandler(() => { |
617 | updateWebserverUrls() | 634 | updateWebserverUrls() |
@@ -681,12 +698,14 @@ export { | |||
681 | RATES_LIMIT, | 698 | RATES_LIMIT, |
682 | MIMETYPES, | 699 | MIMETYPES, |
683 | CRAWL_REQUEST_CONCURRENCY, | 700 | CRAWL_REQUEST_CONCURRENCY, |
701 | DEFAULT_AUDIO_RESOLUTION, | ||
684 | JOB_COMPLETED_LIFETIME, | 702 | JOB_COMPLETED_LIFETIME, |
685 | HTTP_SIGNATURE, | 703 | HTTP_SIGNATURE, |
686 | VIDEO_IMPORT_STATES, | 704 | VIDEO_IMPORT_STATES, |
687 | VIDEO_VIEW_LIFETIME, | 705 | VIDEO_VIEW_LIFETIME, |
688 | CONTACT_FORM_LIFETIME, | 706 | CONTACT_FORM_LIFETIME, |
689 | VIDEO_PLAYLIST_PRIVACIES, | 707 | VIDEO_PLAYLIST_PRIVACIES, |
708 | ASSETS_PATH, | ||
690 | loadLanguages, | 709 | loadLanguages, |
691 | buildLanguages | 710 | buildLanguages |
692 | } | 711 | } |
@@ -700,15 +719,21 @@ function buildVideoMimetypeExt () { | |||
700 | 'video/mp4': '.mp4' | 719 | 'video/mp4': '.mp4' |
701 | } | 720 | } |
702 | 721 | ||
703 | if (CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS) { | 722 | if (CONFIG.TRANSCODING.ENABLED) { |
704 | Object.assign(data, { | 723 | if (CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS) { |
705 | 'video/quicktime': '.mov', | 724 | Object.assign(data, { |
706 | 'video/x-msvideo': '.avi', | 725 | 'video/quicktime': '.mov', |
707 | 'video/x-flv': '.flv', | 726 | 'video/x-msvideo': '.avi', |
708 | 'video/x-matroska': '.mkv', | 727 | 'video/x-flv': '.flv', |
709 | 'application/octet-stream': '.mkv', | 728 | 'video/x-matroska': '.mkv', |
710 | 'video/avi': '.avi' | 729 | 'application/octet-stream': '.mkv', |
711 | }) | 730 | 'video/avi': '.avi' |
731 | }) | ||
732 | } | ||
733 | |||
734 | if (CONFIG.TRANSCODING.ALLOW_AUDIO_FILES) { | ||
735 | Object.assign(data, MIMETYPES.AUDIO.MIMETYPE_EXT) | ||
736 | } | ||
712 | } | 737 | } |
713 | 738 | ||
714 | return data | 739 | return data |
@@ -724,16 +749,15 @@ function updateWebserverUrls () { | |||
724 | } | 749 | } |
725 | 750 | ||
726 | function updateWebserverConfig () { | 751 | function updateWebserverConfig () { |
727 | CONSTRAINTS_FIELDS.VIDEOS.EXTNAME = buildVideosExtname() | ||
728 | |||
729 | MIMETYPES.VIDEO.MIMETYPE_EXT = buildVideoMimetypeExt() | 752 | MIMETYPES.VIDEO.MIMETYPE_EXT = buildVideoMimetypeExt() |
730 | MIMETYPES.VIDEO.EXT_MIMETYPE = invert(MIMETYPES.VIDEO.MIMETYPE_EXT) | 753 | MIMETYPES.VIDEO.EXT_MIMETYPE = invert(MIMETYPES.VIDEO.MIMETYPE_EXT) |
754 | ACTIVITY_PUB.URL_MIME_TYPES.VIDEO = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT) | ||
755 | |||
756 | CONSTRAINTS_FIELDS.VIDEOS.EXTNAME = buildVideosExtname() | ||
731 | } | 757 | } |
732 | 758 | ||
733 | function buildVideosExtname () { | 759 | function buildVideosExtname () { |
734 | return CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS | 760 | return Object.keys(MIMETYPES.VIDEO.EXT_MIMETYPE) |
735 | ? [ '.mp4', '.ogv', '.webm', '.mkv', '.mov', '.avi', '.flv' ] | ||
736 | : [ '.mp4', '.ogv', '.webm' ] | ||
737 | } | 761 | } |
738 | 762 | ||
739 | function loadLanguages () { | 763 | function loadLanguages () { |
diff --git a/server/lib/files-cache/videos-preview-cache.ts b/server/lib/files-cache/videos-preview-cache.ts index 14be7f24a..a68619d07 100644 --- a/server/lib/files-cache/videos-preview-cache.ts +++ b/server/lib/files-cache/videos-preview-cache.ts | |||
@@ -21,7 +21,7 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> { | |||
21 | const video = await VideoModel.loadByUUIDWithFile(videoUUID) | 21 | const video = await VideoModel.loadByUUIDWithFile(videoUUID) |
22 | if (!video) return undefined | 22 | if (!video) return undefined |
23 | 23 | ||
24 | if (video.isOwned()) return { isOwned: true, path: join(CONFIG.STORAGE.PREVIEWS_DIR, video.getPreview().filename) } | 24 | if (video.isOwned()) return { isOwned: true, path: video.getPreview().getPath() } |
25 | 25 | ||
26 | return this.loadRemoteFile(videoUUID) | 26 | return this.loadRemoteFile(videoUUID) |
27 | } | 27 | } |
diff --git a/server/lib/job-queue/handlers/video-file-import.ts b/server/lib/job-queue/handlers/video-file-import.ts index 921d9a083..8cacb0ef3 100644 --- a/server/lib/job-queue/handlers/video-file-import.ts +++ b/server/lib/job-queue/handlers/video-file-import.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import * as Bull from 'bull' | 1 | import * as Bull from 'bull' |
2 | import { logger } from '../../../helpers/logger' | 2 | import { logger } from '../../../helpers/logger' |
3 | import { VideoModel } from '../../../models/video/video' | 3 | import { VideoModel } from '../../../models/video/video' |
4 | import { publishVideoIfNeeded } from './video-transcoding' | 4 | import { publishNewResolutionIfNeeded } from './video-transcoding' |
5 | import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' | 5 | import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' |
6 | import { copy, stat } from 'fs-extra' | 6 | import { copy, stat } from 'fs-extra' |
7 | import { VideoFileModel } from '../../../models/video/video-file' | 7 | import { VideoFileModel } from '../../../models/video/video-file' |
@@ -25,7 +25,7 @@ async function processVideoFileImport (job: Bull.Job) { | |||
25 | 25 | ||
26 | await updateVideoFile(video, payload.filePath) | 26 | await updateVideoFile(video, payload.filePath) |
27 | 27 | ||
28 | await publishVideoIfNeeded(video) | 28 | await publishNewResolutionIfNeeded(video) |
29 | return video | 29 | return video |
30 | } | 30 | } |
31 | 31 | ||
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts index 1650916a6..50e159245 100644 --- a/server/lib/job-queue/handlers/video-import.ts +++ b/server/lib/job-queue/handlers/video-import.ts | |||
@@ -209,6 +209,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide | |||
209 | if (videoImportUpdated.Video.state === VideoState.TO_TRANSCODE) { | 209 | if (videoImportUpdated.Video.state === VideoState.TO_TRANSCODE) { |
210 | // Put uuid because we don't have id auto incremented for now | 210 | // Put uuid because we don't have id auto incremented for now |
211 | const dataInput = { | 211 | const dataInput = { |
212 | type: 'optimize' as 'optimize', | ||
212 | videoUUID: videoImportUpdated.Video.uuid, | 213 | videoUUID: videoImportUpdated.Video.uuid, |
213 | isNewVideo: true | 214 | isNewVideo: true |
214 | } | 215 | } |
diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts index 48cac517e..e9b84ecd6 100644 --- a/server/lib/job-queue/handlers/video-transcoding.ts +++ b/server/lib/job-queue/handlers/video-transcoding.ts | |||
@@ -8,18 +8,39 @@ import { retryTransactionWrapper } from '../../../helpers/database-utils' | |||
8 | import { sequelizeTypescript } from '../../../initializers' | 8 | import { sequelizeTypescript } from '../../../initializers' |
9 | import * as Bluebird from 'bluebird' | 9 | import * as Bluebird from 'bluebird' |
10 | import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' | 10 | import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' |
11 | import { generateHlsPlaylist, optimizeVideofile, transcodeOriginalVideofile } from '../../video-transcoding' | 11 | import { generateHlsPlaylist, optimizeVideofile, transcodeOriginalVideofile, mergeAudioVideofile } from '../../video-transcoding' |
12 | import { Notifier } from '../../notifier' | 12 | import { Notifier } from '../../notifier' |
13 | import { CONFIG } from '../../../initializers/config' | 13 | import { CONFIG } from '../../../initializers/config' |
14 | 14 | ||
15 | export type VideoTranscodingPayload = { | 15 | interface BaseTranscodingPayload { |
16 | videoUUID: string | 16 | videoUUID: string |
17 | resolution?: VideoResolution | ||
18 | isNewVideo?: boolean | 17 | isNewVideo?: boolean |
18 | } | ||
19 | |||
20 | interface HLSTranscodingPayload extends BaseTranscodingPayload { | ||
21 | type: 'hls' | ||
22 | isPortraitMode?: boolean | ||
23 | resolution: VideoResolution | ||
24 | } | ||
25 | |||
26 | interface NewResolutionTranscodingPayload extends BaseTranscodingPayload { | ||
27 | type: 'new-resolution' | ||
19 | isPortraitMode?: boolean | 28 | isPortraitMode?: boolean |
20 | generateHlsPlaylist?: boolean | 29 | resolution: VideoResolution |
30 | } | ||
31 | |||
32 | interface MergeAudioTranscodingPayload extends BaseTranscodingPayload { | ||
33 | type: 'merge-audio' | ||
34 | resolution: VideoResolution | ||
35 | } | ||
36 | |||
37 | interface OptimizeTranscodingPayload extends BaseTranscodingPayload { | ||
38 | type: 'optimize' | ||
21 | } | 39 | } |
22 | 40 | ||
41 | export type VideoTranscodingPayload = HLSTranscodingPayload | NewResolutionTranscodingPayload | ||
42 | | OptimizeTranscodingPayload | MergeAudioTranscodingPayload | ||
43 | |||
23 | async function processVideoTranscoding (job: Bull.Job) { | 44 | async function processVideoTranscoding (job: Bull.Job) { |
24 | const payload = job.data as VideoTranscodingPayload | 45 | const payload = job.data as VideoTranscodingPayload |
25 | logger.info('Processing video file in job %d.', job.id) | 46 | logger.info('Processing video file in job %d.', job.id) |
@@ -31,14 +52,18 @@ async function processVideoTranscoding (job: Bull.Job) { | |||
31 | return undefined | 52 | return undefined |
32 | } | 53 | } |
33 | 54 | ||
34 | if (payload.generateHlsPlaylist) { | 55 | if (payload.type === 'hls') { |
35 | await generateHlsPlaylist(video, payload.resolution, payload.isPortraitMode || false) | 56 | await generateHlsPlaylist(video, payload.resolution, payload.isPortraitMode || false) |
36 | 57 | ||
37 | await retryTransactionWrapper(onHlsPlaylistGenerationSuccess, video) | 58 | await retryTransactionWrapper(onHlsPlaylistGenerationSuccess, video) |
38 | } else if (payload.resolution) { // Transcoding in other resolution | 59 | } else if (payload.type === 'new-resolution') { |
39 | await transcodeOriginalVideofile(video, payload.resolution, payload.isPortraitMode || false) | 60 | await transcodeOriginalVideofile(video, payload.resolution, payload.isPortraitMode || false) |
40 | 61 | ||
41 | await retryTransactionWrapper(publishVideoIfNeeded, video, payload) | 62 | await retryTransactionWrapper(publishNewResolutionIfNeeded, video, payload) |
63 | } else if (payload.type === 'merge-audio') { | ||
64 | await mergeAudioVideofile(video, payload.resolution) | ||
65 | |||
66 | await retryTransactionWrapper(publishNewResolutionIfNeeded, video, payload) | ||
42 | } else { | 67 | } else { |
43 | await optimizeVideofile(video) | 68 | await optimizeVideofile(video) |
44 | 69 | ||
@@ -62,7 +87,7 @@ async function onHlsPlaylistGenerationSuccess (video: VideoModel) { | |||
62 | }) | 87 | }) |
63 | } | 88 | } |
64 | 89 | ||
65 | async function publishVideoIfNeeded (video: VideoModel, payload?: VideoTranscodingPayload) { | 90 | async function publishNewResolutionIfNeeded (video: VideoModel, payload?: NewResolutionTranscodingPayload | MergeAudioTranscodingPayload) { |
66 | const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => { | 91 | const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => { |
67 | // Maybe the video changed in database, refresh it | 92 | // Maybe the video changed in database, refresh it |
68 | let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) | 93 | let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) |
@@ -94,7 +119,7 @@ async function publishVideoIfNeeded (video: VideoModel, payload?: VideoTranscodi | |||
94 | await createHlsJobIfEnabled(payload) | 119 | await createHlsJobIfEnabled(payload) |
95 | } | 120 | } |
96 | 121 | ||
97 | async function onVideoFileOptimizerSuccess (videoArg: VideoModel, payload: VideoTranscodingPayload) { | 122 | async function onVideoFileOptimizerSuccess (videoArg: VideoModel, payload: OptimizeTranscodingPayload) { |
98 | if (videoArg === undefined) return undefined | 123 | if (videoArg === undefined) return undefined |
99 | 124 | ||
100 | // Outside the transaction (IO on disk) | 125 | // Outside the transaction (IO on disk) |
@@ -120,6 +145,7 @@ async function onVideoFileOptimizerSuccess (videoArg: VideoModel, payload: Video | |||
120 | 145 | ||
121 | for (const resolution of resolutionsEnabled) { | 146 | for (const resolution of resolutionsEnabled) { |
122 | const dataInput = { | 147 | const dataInput = { |
148 | type: 'new-resolution' as 'new-resolution', | ||
123 | videoUUID: videoDatabase.uuid, | 149 | videoUUID: videoDatabase.uuid, |
124 | resolution | 150 | resolution |
125 | } | 151 | } |
@@ -149,27 +175,27 @@ async function onVideoFileOptimizerSuccess (videoArg: VideoModel, payload: Video | |||
149 | if (payload.isNewVideo) Notifier.Instance.notifyOnNewVideo(videoDatabase) | 175 | if (payload.isNewVideo) Notifier.Instance.notifyOnNewVideo(videoDatabase) |
150 | if (videoPublished) Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase) | 176 | if (videoPublished) Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase) |
151 | 177 | ||
152 | await createHlsJobIfEnabled(Object.assign({}, payload, { resolution: videoDatabase.getOriginalFile().resolution })) | 178 | const hlsPayload = Object.assign({}, payload, { resolution: videoDatabase.getOriginalFile().resolution }) |
179 | await createHlsJobIfEnabled(hlsPayload) | ||
153 | } | 180 | } |
154 | 181 | ||
155 | // --------------------------------------------------------------------------- | 182 | // --------------------------------------------------------------------------- |
156 | 183 | ||
157 | export { | 184 | export { |
158 | processVideoTranscoding, | 185 | processVideoTranscoding, |
159 | publishVideoIfNeeded | 186 | publishNewResolutionIfNeeded |
160 | } | 187 | } |
161 | 188 | ||
162 | // --------------------------------------------------------------------------- | 189 | // --------------------------------------------------------------------------- |
163 | 190 | ||
164 | function createHlsJobIfEnabled (payload?: VideoTranscodingPayload) { | 191 | function createHlsJobIfEnabled (payload?: { videoUUID: string, resolution: number, isPortraitMode?: boolean }) { |
165 | // Generate HLS playlist? | 192 | // Generate HLS playlist? |
166 | if (payload && CONFIG.TRANSCODING.HLS.ENABLED) { | 193 | if (payload && CONFIG.TRANSCODING.HLS.ENABLED) { |
167 | const hlsTranscodingPayload = { | 194 | const hlsTranscodingPayload = { |
195 | type: 'hls' as 'hls', | ||
168 | videoUUID: payload.videoUUID, | 196 | videoUUID: payload.videoUUID, |
169 | resolution: payload.resolution, | 197 | resolution: payload.resolution, |
170 | isPortraitMode: payload.isPortraitMode, | 198 | isPortraitMode: payload.isPortraitMode |
171 | |||
172 | generateHlsPlaylist: true | ||
173 | } | 199 | } |
174 | 200 | ||
175 | return JobQueue.Instance.createJob({ type: 'video-transcoding', payload: hlsTranscodingPayload }) | 201 | return JobQueue.Instance.createJob({ type: 'video-transcoding', payload: hlsTranscodingPayload }) |
diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts index 950b14c3b..18bdcded4 100644 --- a/server/lib/thumbnail.ts +++ b/server/lib/thumbnail.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { VideoFileModel } from '../models/video/video-file' | 1 | import { VideoFileModel } from '../models/video/video-file' |
2 | import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils' | 2 | import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils' |
3 | import { CONFIG } from '../initializers/config' | 3 | import { CONFIG } from '../initializers/config' |
4 | import { PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants' | 4 | import { PREVIEWS_SIZE, THUMBNAILS_SIZE, ASSETS_PATH } from '../initializers/constants' |
5 | import { VideoModel } from '../models/video/video' | 5 | import { VideoModel } from '../models/video/video' |
6 | import { ThumbnailModel } from '../models/video/thumbnail' | 6 | import { ThumbnailModel } from '../models/video/thumbnail' |
7 | import { ThumbnailType } from '../../shared/models/videos/thumbnail.type' | 7 | import { ThumbnailType } from '../../shared/models/videos/thumbnail.type' |
@@ -45,8 +45,10 @@ function createVideoMiniatureFromExisting (inputPath: string, video: VideoModel, | |||
45 | function generateVideoMiniature (video: VideoModel, videoFile: VideoFileModel, type: ThumbnailType) { | 45 | function generateVideoMiniature (video: VideoModel, videoFile: VideoFileModel, type: ThumbnailType) { |
46 | const input = video.getVideoFilePath(videoFile) | 46 | const input = video.getVideoFilePath(videoFile) |
47 | 47 | ||
48 | const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type) | 48 | const { filename, basePath, height, width, existingThumbnail, outputPath } = buildMetadataFromVideo(video, type) |
49 | const thumbnailCreator = () => generateImageFromVideoFile(input, basePath, filename, { height, width }) | 49 | const thumbnailCreator = videoFile.isAudio() |
50 | ? () => processImage(ASSETS_PATH.DEFAULT_AUDIO_BACKGROUND, outputPath, { width, height }, true) | ||
51 | : () => generateImageFromVideoFile(input, basePath, filename, { height, width }) | ||
50 | 52 | ||
51 | return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail }) | 53 | return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail }) |
52 | } | 54 | } |
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts index d6b6b251a..8d786e0ef 100644 --- a/server/lib/video-transcoding.ts +++ b/server/lib/video-transcoding.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants' | 1 | import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants' |
2 | import { join } from 'path' | 2 | import { join } from 'path' |
3 | import { getVideoFileFPS, transcode, canDoQuickTranscode } from '../helpers/ffmpeg-utils' | 3 | import { canDoQuickTranscode, getVideoFileFPS, transcode, TranscodeOptions, TranscodeOptionsType } from '../helpers/ffmpeg-utils' |
4 | import { ensureDir, move, remove, stat } from 'fs-extra' | 4 | import { ensureDir, move, remove, stat } from 'fs-extra' |
5 | import { logger } from '../helpers/logger' | 5 | import { logger } from '../helpers/logger' |
6 | import { VideoResolution } from '../../shared/models/videos' | 6 | import { VideoResolution } from '../../shared/models/videos' |
@@ -23,13 +23,15 @@ async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFi | |||
23 | const videoInputPath = join(videosDirectory, video.getVideoFilename(inputVideoFile)) | 23 | const videoInputPath = join(videosDirectory, video.getVideoFilename(inputVideoFile)) |
24 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) | 24 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) |
25 | 25 | ||
26 | const doQuickTranscode = await(canDoQuickTranscode(videoInputPath)) | 26 | const transcodeType: TranscodeOptionsType = await canDoQuickTranscode(videoInputPath) |
27 | ? 'quick-transcode' | ||
28 | : 'video' | ||
27 | 29 | ||
28 | const transcodeOptions = { | 30 | const transcodeOptions: TranscodeOptions = { |
31 | type: transcodeType as any, // FIXME: typing issue | ||
29 | inputPath: videoInputPath, | 32 | inputPath: videoInputPath, |
30 | outputPath: videoTranscodedPath, | 33 | outputPath: videoTranscodedPath, |
31 | resolution: inputVideoFile.resolution, | 34 | resolution: inputVideoFile.resolution |
32 | doQuickTranscode | ||
33 | } | 35 | } |
34 | 36 | ||
35 | // Could be very long! | 37 | // Could be very long! |
@@ -39,19 +41,11 @@ async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFi | |||
39 | await remove(videoInputPath) | 41 | await remove(videoInputPath) |
40 | 42 | ||
41 | // Important to do this before getVideoFilename() to take in account the new file extension | 43 | // Important to do this before getVideoFilename() to take in account the new file extension |
42 | inputVideoFile.set('extname', newExtname) | 44 | inputVideoFile.extname = newExtname |
43 | |||
44 | const stats = await stat(videoTranscodedPath) | ||
45 | const fps = await getVideoFileFPS(videoTranscodedPath) | ||
46 | 45 | ||
47 | const videoOutputPath = video.getVideoFilePath(inputVideoFile) | 46 | const videoOutputPath = video.getVideoFilePath(inputVideoFile) |
48 | await move(videoTranscodedPath, videoOutputPath) | ||
49 | 47 | ||
50 | inputVideoFile.set('size', stats.size) | 48 | await onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) |
51 | inputVideoFile.set('fps', fps) | ||
52 | |||
53 | await video.createTorrentAndSetInfoHash(inputVideoFile) | ||
54 | await inputVideoFile.save() | ||
55 | } catch (err) { | 49 | } catch (err) { |
56 | // Auto destruction... | 50 | // Auto destruction... |
57 | video.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', { err })) | 51 | video.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', { err })) |
@@ -81,6 +75,7 @@ async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoR | |||
81 | const videoTranscodedPath = join(transcodeDirectory, video.getVideoFilename(newVideoFile)) | 75 | const videoTranscodedPath = join(transcodeDirectory, video.getVideoFilename(newVideoFile)) |
82 | 76 | ||
83 | const transcodeOptions = { | 77 | const transcodeOptions = { |
78 | type: 'video' as 'video', | ||
84 | inputPath: videoInputPath, | 79 | inputPath: videoInputPath, |
85 | outputPath: videoTranscodedPath, | 80 | outputPath: videoTranscodedPath, |
86 | resolution, | 81 | resolution, |
@@ -89,19 +84,37 @@ async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoR | |||
89 | 84 | ||
90 | await transcode(transcodeOptions) | 85 | await transcode(transcodeOptions) |
91 | 86 | ||
92 | const stats = await stat(videoTranscodedPath) | 87 | return onVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath) |
93 | const fps = await getVideoFileFPS(videoTranscodedPath) | 88 | } |
89 | |||
90 | async function mergeAudioVideofile (video: VideoModel, resolution: VideoResolution) { | ||
91 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | ||
92 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR | ||
93 | const newExtname = '.mp4' | ||
94 | |||
95 | const inputVideoFile = video.getOriginalFile() | ||
94 | 96 | ||
95 | await move(videoTranscodedPath, videoOutputPath) | 97 | const audioInputPath = join(videosDirectory, video.getVideoFilename(video.getOriginalFile())) |
98 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) | ||
96 | 99 | ||
97 | newVideoFile.set('size', stats.size) | 100 | const transcodeOptions = { |
98 | newVideoFile.set('fps', fps) | 101 | type: 'merge-audio' as 'merge-audio', |
102 | inputPath: video.getPreview().getPath(), | ||
103 | outputPath: videoTranscodedPath, | ||
104 | audioPath: audioInputPath, | ||
105 | resolution | ||
106 | } | ||
99 | 107 | ||
100 | await video.createTorrentAndSetInfoHash(newVideoFile) | 108 | await transcode(transcodeOptions) |
101 | 109 | ||
102 | await newVideoFile.save() | 110 | await remove(audioInputPath) |
103 | 111 | ||
104 | video.VideoFiles.push(newVideoFile) | 112 | // Important to do this before getVideoFilename() to take in account the new file extension |
113 | inputVideoFile.extname = newExtname | ||
114 | |||
115 | const videoOutputPath = video.getVideoFilePath(inputVideoFile) | ||
116 | |||
117 | return onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) | ||
105 | } | 118 | } |
106 | 119 | ||
107 | async function generateHlsPlaylist (video: VideoModel, resolution: VideoResolution, isPortraitMode: boolean) { | 120 | async function generateHlsPlaylist (video: VideoModel, resolution: VideoResolution, isPortraitMode: boolean) { |
@@ -112,6 +125,7 @@ async function generateHlsPlaylist (video: VideoModel, resolution: VideoResoluti | |||
112 | const outputPath = join(baseHlsDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution)) | 125 | const outputPath = join(baseHlsDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution)) |
113 | 126 | ||
114 | const transcodeOptions = { | 127 | const transcodeOptions = { |
128 | type: 'hls' as 'hls', | ||
115 | inputPath: videoInputPath, | 129 | inputPath: videoInputPath, |
116 | outputPath, | 130 | outputPath, |
117 | resolution, | 131 | resolution, |
@@ -140,8 +154,34 @@ async function generateHlsPlaylist (video: VideoModel, resolution: VideoResoluti | |||
140 | }) | 154 | }) |
141 | } | 155 | } |
142 | 156 | ||
157 | // --------------------------------------------------------------------------- | ||
158 | |||
143 | export { | 159 | export { |
144 | generateHlsPlaylist, | 160 | generateHlsPlaylist, |
145 | optimizeVideofile, | 161 | optimizeVideofile, |
146 | transcodeOriginalVideofile | 162 | transcodeOriginalVideofile, |
163 | mergeAudioVideofile | ||
164 | } | ||
165 | |||
166 | // --------------------------------------------------------------------------- | ||
167 | |||
168 | async function onVideoFileTranscoding (video: VideoModel, videoFile: VideoFileModel, transcodingPath: string, outputPath: string) { | ||
169 | const stats = await stat(transcodingPath) | ||
170 | const fps = await getVideoFileFPS(transcodingPath) | ||
171 | |||
172 | await move(transcodingPath, outputPath) | ||
173 | |||
174 | videoFile.set('size', stats.size) | ||
175 | videoFile.set('fps', fps) | ||
176 | |||
177 | await video.createTorrentAndSetInfoHash(videoFile) | ||
178 | |||
179 | const updatedVideoFile = await videoFile.save() | ||
180 | |||
181 | // Add it if this is a new created file | ||
182 | if (video.VideoFiles.some(f => f.id === videoFile.id) === false) { | ||
183 | video.VideoFiles.push(updatedVideoFile) | ||
184 | } | ||
185 | |||
186 | return video | ||
147 | } | 187 | } |
diff --git a/server/models/video/thumbnail.ts b/server/models/video/thumbnail.ts index 206e9a3d6..8faf0adba 100644 --- a/server/models/video/thumbnail.ts +++ b/server/models/video/thumbnail.ts | |||
@@ -107,10 +107,12 @@ export class ThumbnailModel extends Model<ThumbnailModel> { | |||
107 | return WEBSERVER.URL + staticPath + this.filename | 107 | return WEBSERVER.URL + staticPath + this.filename |
108 | } | 108 | } |
109 | 109 | ||
110 | removeThumbnail () { | 110 | getPath () { |
111 | const directory = ThumbnailModel.types[this.type].directory | 111 | const directory = ThumbnailModel.types[this.type].directory |
112 | const thumbnailPath = join(directory, this.filename) | 112 | return join(directory, this.filename) |
113 | } | ||
113 | 114 | ||
114 | return remove(thumbnailPath) | 115 | removeThumbnail () { |
116 | return remove(this.getPath()) | ||
115 | } | 117 | } |
116 | } | 118 | } |
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index 2203a7aba..05c490759 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts | |||
@@ -24,6 +24,7 @@ import { VideoModel } from './video' | |||
24 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 24 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
25 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | 25 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' |
26 | import { FindOptions, QueryTypes, Transaction } from 'sequelize' | 26 | import { FindOptions, QueryTypes, Transaction } from 'sequelize' |
27 | import { MIMETYPES } from '../../initializers/constants' | ||
27 | 28 | ||
28 | @Table({ | 29 | @Table({ |
29 | tableName: 'videoFile', | 30 | tableName: 'videoFile', |
@@ -161,6 +162,10 @@ export class VideoFileModel extends Model<VideoFileModel> { | |||
161 | })) | 162 | })) |
162 | } | 163 | } |
163 | 164 | ||
165 | isAudio () { | ||
166 | return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname] | ||
167 | } | ||
168 | |||
164 | hasSameUniqueKeysThan (other: VideoFileModel) { | 169 | hasSameUniqueKeysThan (other: VideoFileModel) { |
165 | return this.fps === other.fps && | 170 | return this.fps === other.fps && |
166 | this.resolution === other.resolution && | 171 | this.resolution === other.resolution && |
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts index 2a2ec606a..8155e11ab 100644 --- a/server/tests/api/check-params/config.ts +++ b/server/tests/api/check-params/config.ts | |||
@@ -59,6 +59,7 @@ describe('Test config API validators', function () { | |||
59 | transcoding: { | 59 | transcoding: { |
60 | enabled: true, | 60 | enabled: true, |
61 | allowAdditionalExtensions: true, | 61 | allowAdditionalExtensions: true, |
62 | allowAudioFiles: true, | ||
62 | threads: 1, | 63 | threads: 1, |
63 | resolutions: { | 64 | resolutions: { |
64 | '240p': false, | 65 | '240p': false, |
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts index ca389b7b6..2ad477c99 100644 --- a/server/tests/api/server/config.ts +++ b/server/tests/api/server/config.ts | |||
@@ -52,6 +52,7 @@ function checkInitialConfig (server: ServerInfo, data: CustomConfig) { | |||
52 | expect(data.user.videoQuotaDaily).to.equal(-1) | 52 | expect(data.user.videoQuotaDaily).to.equal(-1) |
53 | expect(data.transcoding.enabled).to.be.false | 53 | expect(data.transcoding.enabled).to.be.false |
54 | expect(data.transcoding.allowAdditionalExtensions).to.be.false | 54 | expect(data.transcoding.allowAdditionalExtensions).to.be.false |
55 | expect(data.transcoding.allowAudioFiles).to.be.false | ||
55 | expect(data.transcoding.threads).to.equal(2) | 56 | expect(data.transcoding.threads).to.equal(2) |
56 | expect(data.transcoding.resolutions['240p']).to.be.true | 57 | expect(data.transcoding.resolutions['240p']).to.be.true |
57 | expect(data.transcoding.resolutions['360p']).to.be.true | 58 | expect(data.transcoding.resolutions['360p']).to.be.true |
@@ -102,6 +103,7 @@ function checkUpdatedConfig (data: CustomConfig) { | |||
102 | expect(data.transcoding.enabled).to.be.true | 103 | expect(data.transcoding.enabled).to.be.true |
103 | expect(data.transcoding.threads).to.equal(1) | 104 | expect(data.transcoding.threads).to.equal(1) |
104 | expect(data.transcoding.allowAdditionalExtensions).to.be.true | 105 | expect(data.transcoding.allowAdditionalExtensions).to.be.true |
106 | expect(data.transcoding.allowAudioFiles).to.be.true | ||
105 | expect(data.transcoding.resolutions['240p']).to.be.false | 107 | expect(data.transcoding.resolutions['240p']).to.be.false |
106 | expect(data.transcoding.resolutions['360p']).to.be.true | 108 | expect(data.transcoding.resolutions['360p']).to.be.true |
107 | expect(data.transcoding.resolutions['480p']).to.be.true | 109 | expect(data.transcoding.resolutions['480p']).to.be.true |
@@ -215,6 +217,7 @@ describe('Test config', function () { | |||
215 | transcoding: { | 217 | transcoding: { |
216 | enabled: true, | 218 | enabled: true, |
217 | allowAdditionalExtensions: true, | 219 | allowAdditionalExtensions: true, |
220 | allowAudioFiles: true, | ||
218 | threads: 1, | 221 | threads: 1, |
219 | resolutions: { | 222 | resolutions: { |
220 | '240p': false, | 223 | '240p': false, |
diff --git a/shared/extra-utils/server/config.ts b/shared/extra-utils/server/config.ts index deb77e9c0..a5f5989e0 100644 --- a/shared/extra-utils/server/config.ts +++ b/shared/extra-utils/server/config.ts | |||
@@ -91,6 +91,7 @@ function updateCustomSubConfig (url: string, token: string, newConfig: any) { | |||
91 | transcoding: { | 91 | transcoding: { |
92 | enabled: true, | 92 | enabled: true, |
93 | allowAdditionalExtensions: true, | 93 | allowAdditionalExtensions: true, |
94 | allowAudioFiles: true, | ||
94 | threads: 1, | 95 | threads: 1, |
95 | resolutions: { | 96 | resolutions: { |
96 | '240p': false, | 97 | '240p': false, |
diff --git a/shared/models/server/custom-config.model.ts b/shared/models/server/custom-config.model.ts index ca52eff4b..4cc379b2a 100644 --- a/shared/models/server/custom-config.model.ts +++ b/shared/models/server/custom-config.model.ts | |||
@@ -54,6 +54,7 @@ export interface CustomConfig { | |||
54 | transcoding: { | 54 | transcoding: { |
55 | enabled: boolean | 55 | enabled: boolean |
56 | allowAdditionalExtensions: boolean | 56 | allowAdditionalExtensions: boolean |
57 | allowAudioFiles: boolean | ||
57 | threads: number | 58 | threads: number |
58 | resolutions: { | 59 | resolutions: { |
59 | '240p': boolean | 60 | '240p': boolean |