aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/helpers/youtube-dl.ts
diff options
context:
space:
mode:
authorRigel Kent <sendmemail@rigelk.eu>2021-01-15 15:56:56 +0100
committerGitHub <noreply@github.com>2021-01-15 15:56:56 +0100
commit454c20fa7cdb05eba7f1be3c83389b54807af0b3 (patch)
treee9b364da69eaf88f0470414c506c3b5631984c60 /server/helpers/youtube-dl.ts
parentd43c6b1ffc5e6c895f9e9f9de6625f17a9755c20 (diff)
downloadPeerTube-454c20fa7cdb05eba7f1be3c83389b54807af0b3.tar.gz
PeerTube-454c20fa7cdb05eba7f1be3c83389b54807af0b3.tar.zst
PeerTube-454c20fa7cdb05eba7f1be3c83389b54807af0b3.zip
stricter youtubedl format selectors (#3516)
* stricter youtubedl format selectors make sure selectors avoid av1, and otherwise match as closely to the maximum resolution enabled for transcoding * add support for merge formats in youtubedl * avoid vp9.2 in youtubedl to avoid any HDR * move getEnabledResolutions, safer replace of imported extension * add test for youtube-dl selectors
Diffstat (limited to 'server/helpers/youtube-dl.ts')
-rw-r--r--server/helpers/youtube-dl.ts53
1 files changed, 47 insertions, 6 deletions
diff --git a/server/helpers/youtube-dl.ts b/server/helpers/youtube-dl.ts
index 74e5f896c..ebb788e8e 100644
--- a/server/helpers/youtube-dl.ts
+++ b/server/helpers/youtube-dl.ts
@@ -1,6 +1,7 @@
1import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../initializers/constants' 1import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../initializers/constants'
2import { logger } from './logger' 2import { logger } from './logger'
3import { generateVideoImportTmpPath } from './utils' 3import { generateVideoImportTmpPath } from './utils'
4import { getEnabledResolutions } from '../lib/video-transcoding'
4import { join } from 'path' 5import { join } from 'path'
5import { peertubeTruncate, root } from './core-utils' 6import { peertubeTruncate, root } from './core-utils'
6import { ensureDir, remove, writeFile } from 'fs-extra' 7import { ensureDir, remove, writeFile } from 'fs-extra'
@@ -8,6 +9,7 @@ import * as request from 'request'
8import { createWriteStream } from 'fs' 9import { createWriteStream } from 'fs'
9import { CONFIG } from '@server/initializers/config' 10import { CONFIG } from '@server/initializers/config'
10import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' 11import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
12import { VideoResolution } from '../../shared/models/videos'
11 13
12export type YoutubeDLInfo = { 14export type YoutubeDLInfo = {
13 name?: string 15 name?: string
@@ -18,7 +20,8 @@ export type YoutubeDLInfo = {
18 nsfw?: boolean 20 nsfw?: boolean
19 tags?: string[] 21 tags?: string[]
20 thumbnailUrl?: string 22 thumbnailUrl?: string
21 fileExt?: string 23 ext?: string
24 mergeExt?: string
22 originallyPublishedAt?: Date 25 originallyPublishedAt?: Date
23} 26}
24 27
@@ -41,12 +44,21 @@ function getYoutubeDLInfo (url: string, opts?: string[]): Promise<YoutubeDLInfo>
41 } 44 }
42 45
43 args = wrapWithProxyOptions(args) 46 args = wrapWithProxyOptions(args)
47 args = [ '-f', getYoutubeDLVideoFormat() ].concat(args)
44 48
45 safeGetYoutubeDL() 49 safeGetYoutubeDL()
46 .then(youtubeDL => { 50 .then(youtubeDL => {
47 youtubeDL.getInfo(url, args, processOptions, (err, info) => { 51 youtubeDL.getInfo(url, args, processOptions, (err, info) => {
48 if (err) return rej(err) 52 if (err) return rej(err)
49 if (info.is_live === true) return rej(new Error('Cannot download a live streaming.')) 53 if (info.is_live === true) return rej(new Error('Cannot download a live streaming.'))
54 if (info.format_id?.includes('+')) {
55 // this is a merge format and its extension will be appended
56 if (info.ext === 'mp4') {
57 info.mergeExt = 'mp4'
58 } else {
59 info.mergeExt = 'mkv'
60 }
61 }
50 62
51 const obj = buildVideoInfo(normalizeObject(info)) 63 const obj = buildVideoInfo(normalizeObject(info))
52 if (obj.name && obj.name.length < CONSTRAINTS_FIELDS.VIDEOS.NAME.min) obj.name += ' video' 64 if (obj.name && obj.name.length < CONSTRAINTS_FIELDS.VIDEOS.NAME.min) obj.name += ' video'
@@ -92,13 +104,40 @@ function getYoutubeDLSubs (url: string, opts?: object): Promise<YoutubeDLSubs> {
92 }) 104 })
93} 105}
94 106
95function downloadYoutubeDLVideo (url: string, extension: string, timeout: number) { 107function getYoutubeDLVideoFormat () {
108 /**
109 * list of format selectors in order or preference
110 * see https://github.com/ytdl-org/youtube-dl#format-selection
111 *
112 * case #1 asks for a mp4 using h264 (avc1) and the exact resolution in the hope
113 * of being able to do a "quick-transcode"
114 * case #2 is the first fallback. No "quick-transcode" means we can get anything else (like vp9)
115 * case #3 is the resolution-degraded equivalent of #1, and already a pretty safe fallback
116 *
117 * in any case we avoid AV1, see https://github.com/Chocobozzz/PeerTube/issues/3499
118 **/
119 const enabledResolutions = getEnabledResolutions('vod')
120 const resolution = enabledResolutions.length === 0
121 ? VideoResolution.H_720P
122 : Math.max(...enabledResolutions)
123
124 return [
125 `bestvideo[vcodec^=avc1][height=${resolution}]+bestaudio[ext=m4a]`, // case #1
126 `bestvideo[vcodec!*=av01][vcodec!*=vp9.2][height=${resolution}]+bestaudio`, // case #2
127 `bestvideo[vcodec^=avc1][height<=${resolution}]+bestaudio[ext=m4a]`, // case #3
128 `bestvideo[vcodec!*=av01][vcodec!*=vp9.2]+bestaudio`,
129 'best[vcodec!*=av01][vcodec!*=vp9.2]' // case fallback
130 ].join('/')
131}
132
133function downloadYoutubeDLVideo (url: string, extension: string, timeout: number, mergeExtension?: string) {
96 const path = generateVideoImportTmpPath(url, extension) 134 const path = generateVideoImportTmpPath(url, extension)
135 const finalPath = mergeExtension ? path.replace(new RegExp(`${extension}$`), mergeExtension) : path
97 let timer 136 let timer
98 137
99 logger.info('Importing youtubeDL video %s to %s', url, path) 138 logger.info('Importing youtubeDL video %s to %s', url, finalPath)
100 139
101 let options = [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', '-o', path ] 140 let options = [ '-f', getYoutubeDLVideoFormat(), '-o', path ]
102 options = wrapWithProxyOptions(options) 141 options = wrapWithProxyOptions(options)
103 142
104 if (process.env.FFMPEG_PATH) { 143 if (process.env.FFMPEG_PATH) {
@@ -118,7 +157,7 @@ function downloadYoutubeDLVideo (url: string, extension: string, timeout: number
118 return rej(err) 157 return rej(err)
119 } 158 }
120 159
121 return res(path) 160 return res(finalPath)
122 }) 161 })
123 162
124 timer = setTimeout(() => { 163 timer = setTimeout(() => {
@@ -236,6 +275,7 @@ function buildOriginallyPublishedAt (obj: any) {
236 275
237export { 276export {
238 updateYoutubeDLBinary, 277 updateYoutubeDLBinary,
278 getYoutubeDLVideoFormat,
239 downloadYoutubeDLVideo, 279 downloadYoutubeDLVideo,
240 getYoutubeDLSubs, 280 getYoutubeDLSubs,
241 getYoutubeDLInfo, 281 getYoutubeDLInfo,
@@ -275,7 +315,8 @@ function buildVideoInfo (obj: any): YoutubeDLInfo {
275 tags: getTags(obj.tags), 315 tags: getTags(obj.tags),
276 thumbnailUrl: obj.thumbnail || undefined, 316 thumbnailUrl: obj.thumbnail || undefined,
277 originallyPublishedAt: buildOriginallyPublishedAt(obj), 317 originallyPublishedAt: buildOriginallyPublishedAt(obj),
278 fileExt: obj.ext 318 ext: obj.ext,
319 mergeExt: obj.mergeExt
279 } 320 }
280} 321}
281 322