<ng-template [ngIf]="isTranscodingEnabled()">
+ <div class="form-group">
+ <my-peertube-checkbox
+ inputName="transcodingAllowAdditionalExtensions" formControlName="transcodingAllowAdditionalExtensions"
+ i18n-labelText labelText="Allow additional extensions"
+ i18n-helpHtml helpHtml="Allow your users to upload .mkv, .mov, .avi, .flv videos"
+ ></my-peertube-checkbox>
+ </div>
+
<div class="form-group">
<label i18n for="transcodingThreads">Transcoding threads</label>
<div class="peertube-select-container">
userVideoQuota: this.userValidatorsService.USER_VIDEO_QUOTA,
userVideoQuotaDaily: this.userValidatorsService.USER_VIDEO_QUOTA_DAILY,
transcodingThreads: this.customConfigValidatorsService.TRANSCODING_THREADS,
+ transcodingAllowAdditionalExtensions: null,
transcodingEnabled: null,
customizationJavascript: null,
customizationCSS: null
},
transcoding: {
enabled: this.form.value['transcodingEnabled'],
+ allowAdditionalExtensions: this.form.value['transcodingAllowAdditionalExtensions'],
threads: this.form.value['transcodingThreads'],
resolutions: {
'240p': this.form.value[this.getResolutionKey('240p')],
userVideoQuotaDaily: this.customConfig.user.videoQuotaDaily,
transcodingThreads: this.customConfig.transcoding.threads,
transcodingEnabled: this.customConfig.transcoding.enabled,
+ transcodingAllowAdditionalExtensions: this.customConfig.transcoding.allowAdditionalExtensions,
customizationJavascript: this.customConfig.instance.customizations.javascript,
customizationCSS: this.customConfig.instance.customizations.css,
importVideosHttpEnabled: this.customConfig.import.videos.http.enabled,
<div class="root">
<label class="form-group-checkbox">
- <input type="checkbox" [(ngModel)]="checked" (ngModelChange)="onModelChange()" [id]="inputName" [disabled]="isDisabled" />
+ <input type="checkbox" [(ngModel)]="checked" (ngModelChange)="onModelChange()" [id]="inputName" [disabled]="disabled" />
<span role="checkbox" [attr.aria-checked]="checked"></span>
<span *ngIf="labelText">{{ labelText }}</span>
<span *ngIf="labelHtml" [innerHTML]="labelHtml"></span>
</label>
<my-help *ngIf="helpHtml" tooltipPlacement="top" helpType="custom" i18n-customHtml [customHtml]="helpHtml"></my-help>
-</div>
\ No newline at end of file
+</div>
@Input() labelText: string
@Input() labelHtml: string
@Input() helpHtml: string
-
- isDisabled = false
+ @Input() disabled = false
propagateChange = (_: any) => { /* empty */ }
}
setDisabledState (isDisabled: boolean) {
- this.isDisabled = isDisabled
+ this.disabled = isDisabled
}
}
></my-peertube-checkbox>
<my-peertube-checkbox
+ *ngIf="waitTranscodingEnabled"
inputName="waitTranscoding" formControlName="waitTranscoding"
i18n-labelText labelText="Wait transcoding before publishing the video"
i18n-helpHtml helpHtml="If you decide not to wait for transcoding before publishing the video, it could be unplayable until transcoding ends."
@Input() userVideoChannels: { id: number, label: string, support: string }[] = []
@Input() schedulePublicationPossible = true
@Input() videoCaptions: VideoCaptionEdit[] = []
+ @Input() waitTranscodingEnabled = true
@ViewChild('videoCaptionAddModal') videoCaptionAddModal: VideoCaptionAddModalComponent
<span i18n>Select the file to upload</span>
<input #videofileInput type="file" name="videofile" id="videofile" [accept]="videoExtensions" (change)="fileChange()" />
</div>
- <span class="button-file-extension">(.mp4, .webm, .ogv)</span>
+ <span class="button-file-extension">({{ videoExtensions }})</span>
<div class="form-group form-group-channel">
<label i18n for="first-step-channel">Channel</label>
<my-video-edit
[form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions"
[validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels"
+ [waitTranscodingEnabled]="waitTranscodingEnabled"
></my-video-edit>
<div class="submit-container">
id: 0,
uuid: ''
}
+ waitTranscodingEnabled = true
error: string
const videofile = this.videofileInput.nativeElement.files[0]
if (!videofile) return
+ // Check global user quota
const bytePipes = new BytesPipe()
const videoQuota = this.authService.getUser().videoQuota
if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) {
return
}
+ // Check daily user quota
const videoQuotaDaily = this.authService.getUser().videoQuotaDaily
if (videoQuotaDaily !== -1 && (this.userVideoQuotaUsedDaily + videofile.size) > videoQuotaDaily) {
const msg = this.i18n(
return
}
+ // Build name field
const nameWithoutExtension = videofile.name.replace(/\.[^/.]+$/, '')
let name: string
if (nameWithoutExtension.length < 3) name = videofile.name
else name = nameWithoutExtension
+ // Force user to wait transcoding for unsupported video types in web browsers
+ if (!videofile.name.endsWith('.mp4') && !videofile.name.endsWith('.webm') && !videofile.name.endsWith('.ogv')) {
+ this.waitTranscodingEnabled = false
+ }
+
const privacy = this.firstStepPrivacyId.toString()
const nsfw = false
const waitTranscoding = true
<my-video-edit
[form]="form" [formErrors]="formErrors" [schedulePublicationPossible]="schedulePublicationPossible"
[validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels"
- [videoCaptions]="videoCaptions"
+ [videoCaptions]="videoCaptions" [waitTranscodingEnabled]="waitTranscodingEnabled"
></my-video-edit>
<div class="submit-container">
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
import { VideoCaptionService } from '@app/shared/video-caption'
import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model'
+import { VideoDetails } from '@app/shared/video/video-details.model'
@Component({
selector: 'my-videos-update',
userVideoChannels: { id: number, label: string, support: string }[] = []
schedulePublicationPossible = false
videoCaptions: VideoCaptionEdit[] = []
+ waitTranscodingEnabled = true
private updateDone = false
this.videoPrivacies = this.videoService.explainedPrivacyLabels(this.videoPrivacies)
+ const videoFiles = (video as VideoDetails).files
+ if (videoFiles.length > 1) { // Already transcoded
+ this.waitTranscodingEnabled = false
+ }
+
// FIXME: Angular does not detect the change inside this subscription, so use the patched setTimeout
setTimeout(() => this.hydrateFormFromVideo())
},
# Please, do not disable transcoding since many uploaded videos will not work
transcoding:
enabled: true
+ # Allow your users to upload .mkv, .mov, .avi, .flv videos
+ allow_additional_extensions: true
threads: 1
resolutions: # Only created if the original video has a higher resolution, uses more storage!
240p: false
# Please, do not disable transcoding since many uploaded videos will not work
transcoding:
enabled: true
+ # Allow your users to upload .mkv, .mov, .avi, .flv videos
+ allow_additional_extensions: true
threads: 1
resolutions: # Only created if the original video has a higher resolution, uses more storage!
240p: false
transcoding:
enabled: true
+ allow_additional_extensions: true
transcoding:
enabled: true
+ allow_additional_extensions: false
threads: 2
resolutions:
240p: true
'instance.defaultClientRoute',
'instance.shortDescription',
'cache.videoCaptions',
- 'signup.requiresEmailVerification'
+ 'signup.requiresEmailVerification',
+ 'transcoding.allowAdditionalExtensions'
)
toUpdateJSON.user['video_quota'] = toUpdate.user.videoQuota
toUpdateJSON.user['video_quota_daily'] = toUpdate.user.videoQuotaDaily
toUpdateJSON.instance['short_description'] = toUpdate.instance.shortDescription
toUpdateJSON.instance['default_nsfw_policy'] = toUpdate.instance.defaultNSFWPolicy
toUpdateJSON.signup['requires_email_verification'] = toUpdate.signup.requiresEmailVerification
+ toUpdateJSON.transcoding['allow_additional_extensions'] = toUpdate.transcoding.allowAdditionalExtensions
await writeJSON(CONFIG.CUSTOM_FILE, toUpdateJSON, { spaces: 2 })
},
transcoding: {
enabled: CONFIG.TRANSCODING.ENABLED,
+ allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS,
threads: CONFIG.TRANSCODING.THREADS,
resolutions: {
'240p': CONFIG.TRANSCODING.RESOLUTIONS[ '240p' ],
import 'multer'
import { UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../../shared'
import { getFormattedObjects } from '../../../helpers/utils'
-import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../../initializers'
+import { CONFIG, MIMETYPES, sequelizeTypescript } from '../../../initializers'
import { sendUpdateActor } from '../../../lib/activitypub/send'
import {
asyncMiddleware,
const auditLogger = auditLoggerFactory('users-me')
-const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR })
+const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR })
const meRouter = express.Router()
import { buildNSFWFilter, createReqFiles, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
import { setAsyncActorKeys } from '../../lib/activitypub'
import { AccountModel } from '../../models/account/account'
-import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers'
+import { CONFIG, MIMETYPES, sequelizeTypescript } from '../../initializers'
import { logger } from '../../helpers/logger'
import { VideoModel } from '../../models/video/video'
import { updateAvatarValidator } from '../../middlewares/validators/avatar'
import { UserModel } from '../../models/account/user'
const auditLogger = auditLoggerFactory('channels')
-const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR })
+const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR })
const videoChannelRouter = express.Router()
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares'
import { addVideoCaptionValidator, deleteVideoCaptionValidator, listVideoCaptionsValidator } from '../../../middlewares/validators'
import { createReqFiles } from '../../../helpers/express-utils'
-import { CONFIG, sequelizeTypescript, VIDEO_CAPTIONS_MIMETYPE_EXT } from '../../../initializers'
+import { CONFIG, MIMETYPES, sequelizeTypescript } from '../../../initializers'
import { getFormattedObjects } from '../../../helpers/utils'
import { VideoCaptionModel } from '../../../models/video/video-caption'
import { VideoModel } from '../../../models/video/video'
const reqVideoCaptionAdd = createReqFiles(
[ 'captionfile' ],
- VIDEO_CAPTIONS_MIMETYPE_EXT,
+ MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT,
{
captionfile: CONFIG.STORAGE.CAPTIONS_DIR
}
import 'multer'
import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger'
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares'
-import {
- CONFIG,
- IMAGE_MIMETYPE_EXT,
- PREVIEWS_SIZE,
- sequelizeTypescript,
- THUMBNAILS_SIZE,
- TORRENT_MIMETYPE_EXT
-} from '../../../initializers'
+import { CONFIG, MIMETYPES, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE } from '../../../initializers'
import { getYoutubeDLInfo, YoutubeDLInfo } from '../../../helpers/youtube-dl'
import { createReqFiles } from '../../../helpers/express-utils'
import { logger } from '../../../helpers/logger'
const reqVideoFileImport = createReqFiles(
[ 'thumbnailfile', 'previewfile', 'torrentfile' ],
- Object.assign({}, TORRENT_MIMETYPE_EXT, IMAGE_MIMETYPE_EXT),
+ Object.assign({}, MIMETYPES.TORRENT.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT),
{
thumbnailfile: CONFIG.STORAGE.TMP_DIR,
previewfile: CONFIG.STORAGE.TMP_DIR,
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
import { getFormattedObjects, getServerActor } from '../../../helpers/utils'
import {
- CONFIG,
- IMAGE_MIMETYPE_EXT,
+ CONFIG, MIMETYPES,
PREVIEWS_SIZE,
sequelizeTypescript,
THUMBNAILS_SIZE,
VIDEO_CATEGORIES,
VIDEO_LANGUAGES,
VIDEO_LICENCES,
- VIDEO_MIMETYPE_EXT,
VIDEO_PRIVACIES
} from '../../../initializers'
import {
import { videoCaptionsRouter } from './captions'
import { videoImportsRouter } from './import'
import { resetSequelizeInstance } from '../../../helpers/database-utils'
-import { rename } from 'fs-extra'
+import { move } from 'fs-extra'
import { watchingRouter } from './watching'
const auditLogger = auditLoggerFactory('videos')
const reqVideoFileAdd = createReqFiles(
[ 'videofile', 'thumbnailfile', 'previewfile' ],
- Object.assign({}, VIDEO_MIMETYPE_EXT, IMAGE_MIMETYPE_EXT),
+ Object.assign({}, MIMETYPES.VIDEO.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT),
{
videofile: CONFIG.STORAGE.TMP_DIR,
thumbnailfile: CONFIG.STORAGE.TMP_DIR,
)
const reqVideoFileUpdate = createReqFiles(
[ 'thumbnailfile', 'previewfile' ],
- IMAGE_MIMETYPE_EXT,
+ MIMETYPES.IMAGE.MIMETYPE_EXT,
{
thumbnailfile: CONFIG.STORAGE.TMP_DIR,
previewfile: CONFIG.STORAGE.TMP_DIR
// Move physical file
const videoDir = CONFIG.STORAGE.VIDEOS_DIR
const destination = join(videoDir, video.getVideoFilename(videoFile))
- await rename(videoPhysicalFile.path, destination)
+ await move(videoPhysicalFile.path, destination)
// This is important in case if there is another attempt in the retry process
videoPhysicalFile.filename = video.getVideoFilename(videoFile)
videoPhysicalFile.path = destination
-import { CONSTRAINTS_FIELDS, VIDEO_CAPTIONS_MIMETYPE_EXT, VIDEO_LANGUAGES } from '../../initializers'
+import { CONSTRAINTS_FIELDS, MIMETYPES, VIDEO_LANGUAGES } from '../../initializers'
import { exists, isFileValid } from './misc'
import { Response } from 'express'
import { VideoModel } from '../../models/video/video'
return exists(value) && VIDEO_LANGUAGES[ value ] !== undefined
}
-const videoCaptionTypes = Object.keys(VIDEO_CAPTIONS_MIMETYPE_EXT)
+const videoCaptionTypes = Object.keys(MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT)
.concat([ 'application/octet-stream' ]) // MacOS sends application/octet-stream ><
.map(m => `(${m})`)
const videoCaptionTypesRegex = videoCaptionTypes.join('|')
import 'express-validator'
import 'multer'
import * as validator from 'validator'
-import { CONSTRAINTS_FIELDS, TORRENT_MIMETYPE_EXT, VIDEO_IMPORT_STATES } from '../../initializers'
+import { CONSTRAINTS_FIELDS, MIMETYPES, VIDEO_IMPORT_STATES } from '../../initializers'
import { exists, isFileValid } from './misc'
import * as express from 'express'
import { VideoImportModel } from '../../models/video/video-import'
return exists(value) && VIDEO_IMPORT_STATES[ value ] !== undefined
}
-const videoTorrentImportTypes = Object.keys(TORRENT_MIMETYPE_EXT).map(m => `(${m})`)
+const videoTorrentImportTypes = Object.keys(MIMETYPES.TORRENT.MIMETYPE_EXT).map(m => `(${m})`)
const videoTorrentImportRegex = videoTorrentImportTypes.join('|')
function isVideoImportTorrentFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) {
return isFileValid(files, videoTorrentImportRegex, 'torrentfile', CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_FILE.FILE_SIZE.max, true)
import * as validator from 'validator'
import { UserRight, VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared'
import {
- CONSTRAINTS_FIELDS,
+ CONSTRAINTS_FIELDS, MIMETYPES,
VIDEO_CATEGORIES,
VIDEO_LICENCES,
- VIDEO_MIMETYPE_EXT,
VIDEO_PRIVACIES,
VIDEO_RATE_TYPES,
VIDEO_STATES
return value === 'none' || values(VIDEO_RATE_TYPES).indexOf(value as VideoRateType) !== -1
}
-const videoFileTypes = Object.keys(VIDEO_MIMETYPE_EXT).map(m => `(${m})`)
-const videoFileTypesRegex = videoFileTypes.join('|')
+function isVideoFileExtnameValid (value: string) {
+ return exists(value) && MIMETYPES.VIDEO.EXT_MIMETYPE[value] !== undefined
+}
function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) {
+ const videoFileTypesRegex = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT)
+ .map(m => `(${m})`)
+ .join('|')
+
return isFileValid(files, videoFileTypesRegex, 'videofile', null)
}
isVideoStateValid,
isVideoViewsValid,
isVideoRatingTypeValid,
+ isVideoFileExtnameValid,
isVideoDurationValid,
isVideoTagValid,
isVideoPrivacyValid,
'signup.enabled', 'signup.limit', 'signup.requires_email_verification',
'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist',
'redundancy.videos.strategies', 'redundancy.videos.check_interval',
- 'transcoding.enabled', 'transcoding.threads',
+ 'transcoding.enabled', 'transcoding.threads', 'transcoding.allow_additional_extensions',
'import.videos.http.enabled', 'import.videos.torrent.enabled',
'trending.videos.interval_days',
'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route',
// ---------------------------------------------------------------------------
-const LAST_MIGRATION_VERSION = 290
+const LAST_MIGRATION_VERSION = 295
// ---------------------------------------------------------------------------
},
TRANSCODING: {
get ENABLED () { return config.get<boolean>('transcoding.enabled') },
+ get ALLOW_ADDITIONAL_EXTENSIONS () { return config.get<boolean>('transcoding.allow_additional_extensions') },
get THREADS () { return config.get<number>('transcoding.threads') },
RESOLUTIONS: {
get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') },
// ---------------------------------------------------------------------------
-const CONSTRAINTS_FIELDS = {
+let CONSTRAINTS_FIELDS = {
USERS: {
NAME: { min: 1, max: 50 }, // Length
DESCRIPTION: { min: 3, max: 1000 }, // Length
max: 2 * 1024 * 1024 // 2MB
}
},
- EXTNAME: [ '.mp4', '.ogv', '.webm' ],
+ EXTNAME: buildVideosExtname(),
INFO_HASH: { min: 40, max: 40 }, // Length, info hash is 20 bytes length but we represent it in hexadecimal so 20 * 2
DURATION: { min: 0 }, // Number
TAGS: { min: 0, max: 5 }, // Number of total tags
[VideoAbuseState.ACCEPTED]: 'Accepted'
}
-const VIDEO_MIMETYPE_EXT = {
- 'video/webm': '.webm',
- 'video/ogg': '.ogv',
- 'video/mp4': '.mp4'
-}
-const VIDEO_EXT_MIMETYPE = invert(VIDEO_MIMETYPE_EXT)
-
-const IMAGE_MIMETYPE_EXT = {
- 'image/png': '.png',
- 'image/jpg': '.jpg',
- 'image/jpeg': '.jpg'
-}
-
-const VIDEO_CAPTIONS_MIMETYPE_EXT = {
- 'text/vtt': '.vtt',
- 'application/x-subrip': '.srt'
-}
-
-const TORRENT_MIMETYPE_EXT = {
- 'application/x-bittorrent': '.torrent'
+const MIMETYPES = {
+ VIDEO: {
+ MIMETYPE_EXT: buildVideoMimetypeExt(),
+ EXT_MIMETYPE: null as { [ id: string ]: string }
+ },
+ IMAGE: {
+ MIMETYPE_EXT: {
+ 'image/png': '.png',
+ 'image/jpg': '.jpg',
+ 'image/jpeg': '.jpg'
+ }
+ },
+ VIDEO_CAPTIONS: {
+ MIMETYPE_EXT: {
+ 'text/vtt': '.vtt',
+ 'application/x-subrip': '.srt'
+ }
+ },
+ TORRENT: {
+ MIMETYPE_EXT: {
+ 'application/x-bittorrent': '.torrent'
+ }
+ }
}
+MIMETYPES.VIDEO.EXT_MIMETYPE = invert(MIMETYPES.VIDEO.MIMETYPE_EXT)
// ---------------------------------------------------------------------------
COLLECTION_ITEMS_PER_PAGE: 10,
FETCH_PAGE_LIMIT: 100,
URL_MIME_TYPES: {
- VIDEO: Object.keys(VIDEO_MIMETYPE_EXT),
+ VIDEO: Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT),
TORRENT: [ 'application/x-bittorrent' ],
MAGNET: [ 'application/x-bittorrent;x-scheme-handler/magnet' ]
},
ROUTE_CACHE_LIFETIME.OVERVIEWS.VIDEOS = '0ms'
}
-updateWebserverConfig()
+updateWebserverUrls()
// ---------------------------------------------------------------------------
export {
API_VERSION,
- VIDEO_CAPTIONS_MIMETYPE_EXT,
AVATARS_SIZE,
ACCEPT_HEADERS,
BCRYPT_SALT_SIZE,
FEEDS,
JOB_TTL,
NSFW_POLICY_TYPES,
- TORRENT_MIMETYPE_EXT,
STATIC_MAX_AGE,
STATIC_PATHS,
VIDEO_IMPORT_TIMEOUT,
VIDEO_LICENCES,
VIDEO_STATES,
VIDEO_RATE_TYPES,
- VIDEO_MIMETYPE_EXT,
VIDEO_TRANSCODING_FPS,
FFMPEG_NICE,
VIDEO_ABUSE_STATES,
USER_PASSWORD_RESET_LIFETIME,
MEMOIZE_TTL,
USER_EMAIL_VERIFY_LIFETIME,
- IMAGE_MIMETYPE_EXT,
OVERVIEWS,
SCHEDULER_INTERVALS_MS,
REPEAT_JOBS,
STATIC_DOWNLOAD_PATHS,
RATES_LIMIT,
- VIDEO_EXT_MIMETYPE,
+ MIMETYPES,
CRAWL_REQUEST_CONCURRENCY,
JOB_COMPLETED_LIFETIME,
HTTP_SIGNATURE,
return join(dirname(configSources[ 0 ].name), filename + '.json')
}
-function updateWebserverConfig () {
+function buildVideoMimetypeExt () {
+ const data = {
+ 'video/webm': '.webm',
+ 'video/ogg': '.ogv',
+ 'video/mp4': '.mp4'
+ }
+
+ if (CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS) {
+ Object.assign(data, {
+ 'video/quicktime': '.mov',
+ 'video/x-msvideo': '.avi',
+ 'video/x-flv': '.flv',
+ 'video/x-matroska': '.mkv'
+ })
+ }
+
+ return data
+}
+
+function updateWebserverUrls () {
CONFIG.WEBSERVER.URL = sanitizeUrl(CONFIG.WEBSERVER.SCHEME + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT)
CONFIG.WEBSERVER.HOST = sanitizeHost(CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT, REMOTE_SCHEME.HTTP)
}
+function updateWebserverConfig () {
+ CONSTRAINTS_FIELDS.VIDEOS.EXTNAME = buildVideosExtname()
+
+ MIMETYPES.VIDEO.MIMETYPE_EXT = buildVideoMimetypeExt()
+ MIMETYPES.VIDEO.EXT_MIMETYPE = invert(MIMETYPES.VIDEO.MIMETYPE_EXT)
+}
+
+function buildVideosExtname () {
+ return CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS
+ ? [ '.mp4', '.ogv', '.webm', '.mkv', '.mov', '.avi', '.flv' ]
+ : [ '.mp4', '.ogv', '.webm' ]
+}
+
function buildVideosRedundancy (objs: any[]): VideosRedundancy[] {
if (!objs) return []
config = require('config')
updateWebserverConfig()
+ updateWebserverUrls()
}
--- /dev/null
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+ transaction: Sequelize.Transaction,
+ queryInterface: Sequelize.QueryInterface,
+ sequelize: Sequelize.Sequelize,
+ db: any
+}): Promise<void> {
+ {
+ await utils.queryInterface.renameColumn('videoFile', 'extname', 'extname_old')
+ }
+
+ {
+ const data = {
+ type: Sequelize.STRING,
+ defaultValue: null,
+ allowNull: true
+ }
+
+ await utils.queryInterface.addColumn('videoFile', 'extname', data)
+ }
+
+ {
+ const query = 'UPDATE "videoFile" SET "extname" = "extname_old"::text'
+ await utils.sequelize.query(query)
+ }
+
+ {
+ const data = {
+ type: Sequelize.STRING,
+ defaultValue: null,
+ allowNull: false
+ }
+ await utils.queryInterface.changeColumn('videoFile', 'extname', data)
+ }
+
+ {
+ await utils.queryInterface.removeColumn('videoFile', 'extname_old')
+ }
+}
+
+function down (options) {
+ throw new Error('Not implemented.')
+}
+
+export {
+ up,
+ down
+}
import * as Bluebird from 'bluebird'
-import { join } from 'path'
import { Transaction } from 'sequelize'
import * as url from 'url'
import * as uuidv4 from 'uuid/v4'
import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
import { doRequest, downloadImage } from '../../helpers/requests'
import { getUrlFromWebfinger } from '../../helpers/webfinger'
-import { AVATARS_SIZE, CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers'
+import { AVATARS_SIZE, CONFIG, MIMETYPES, sequelizeTypescript } from '../../initializers'
import { AccountModel } from '../../models/account/account'
import { ActorModel } from '../../models/activitypub/actor'
import { AvatarModel } from '../../models/avatar/avatar'
async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
if (
- actorJSON.icon && actorJSON.icon.type === 'Image' && IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
+ actorJSON.icon && actorJSON.icon.type === 'Image' && MIMETYPES.IMAGE.MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
isActivityPubUrlValid(actorJSON.icon.url)
) {
- const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType]
+ const extension = MIMETYPES.IMAGE.MIMETYPE_EXT[actorJSON.icon.mediaType]
const avatarName = uuidv4() + extension
await downloadImage(actorJSON.icon.url, CONFIG.STORAGE.AVATARS_DIR, avatarName, AVATARS_SIZE)
import * as Bluebird from 'bluebird'
import * as sequelize from 'sequelize'
import * as magnetUtil from 'magnet-uri'
-import { join } from 'path'
import * as request from 'request'
import { ActivityIconObject, ActivityUrlObject, ActivityVideoUrlObject, VideoState } from '../../../shared/index'
import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
import { logger } from '../../helpers/logger'
import { doRequest, downloadImage } from '../../helpers/requests'
-import { ACTIVITY_PUB, CONFIG, REMOTE_SCHEME, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_MIMETYPE_EXT } from '../../initializers'
+import { ACTIVITY_PUB, CONFIG, MIMETYPES, REMOTE_SCHEME, sequelizeTypescript, THUMBNAILS_SIZE } from '../../initializers'
import { ActorModel } from '../../models/activitypub/actor'
import { TagModel } from '../../models/video/tag'
import { VideoModel } from '../../models/video/video'
// ---------------------------------------------------------------------------
function isActivityVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject {
- const mimeTypes = Object.keys(VIDEO_MIMETYPE_EXT)
+ const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT)
const urlMediaType = url.mediaType || url.mimeType
return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/')
const mediaType = fileUrl.mediaType || fileUrl.mimeType
const attribute = {
- extname: VIDEO_MIMETYPE_EXT[ mediaType ],
+ extname: MIMETYPES.VIDEO.MIMETYPE_EXT[ mediaType ],
infoHash: parsed.infoHash,
resolution: fileUrl.height,
size: fileUrl.size,
import { ActorModel } from '../activitypub/actor'
import { getVideoSort, throwIfNotValid } from '../utils'
import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc'
-import { CONFIG, CONSTRAINTS_FIELDS, STATIC_PATHS, VIDEO_EXT_MIMETYPE } from '../../initializers'
+import { CONFIG, CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers'
import { VideoFileModel } from '../video/video-file'
import { getServerActor } from '../../helpers/utils'
import { VideoModel } from '../video/video'
expires: this.expiresOn.toISOString(),
url: {
type: 'Link',
- mimeType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any,
- mediaType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any,
+ mimeType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any,
+ mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any,
href: this.fileUrl,
height: this.VideoFile.resolution,
size: this.VideoFile.size,
UpdatedAt
} from 'sequelize-typescript'
import {
+ isVideoFileExtnameValid,
isVideoFileInfoHashValid,
isVideoFileResolutionValid,
isVideoFileSizeValid,
size: number
@AllowNull(false)
- @Column(DataType.ENUM(values(CONSTRAINTS_FIELDS.VIDEOS.EXTNAME)))
+ @Is('VideoFileExtname', value => throwIfNotValid(value, isVideoFileExtnameValid, 'extname'))
+ @Column
extname: string
@AllowNull(false)
import { VideoModel } from './video'
import { VideoFileModel } from './video-file'
import { ActivityUrlObject, VideoTorrentObject } from '../../../shared/models/activitypub/objects'
-import { CONFIG, THUMBNAILS_SIZE, VIDEO_EXT_MIMETYPE } from '../../initializers'
+import { CONFIG, MIMETYPES, THUMBNAILS_SIZE } from '../../initializers'
import { VideoCaptionModel } from './video-caption'
import {
getVideoCommentsActivityPubUrl,
for (const file of video.VideoFiles) {
url.push({
type: 'Link',
- mimeType: VIDEO_EXT_MIMETYPE[ file.extname ] as any,
- mediaType: VIDEO_EXT_MIMETYPE[ file.extname ] as any,
+ mimeType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any,
+ mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any,
href: video.getVideoFileUrl(file, baseUrlHttp),
height: file.resolution,
size: file.size,
},
transcoding: {
enabled: true,
+ allowAdditionalExtensions: true,
threads: 1,
resolutions: {
'240p': false,
it('Should fail without an incorrect input file', async function () {
const fields = baseCorrectParams
- const attaches = {
+ let attaches = {
'videofile': join(__dirname, '..', '..', 'fixtures', 'video_short_fake.webm')
}
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
+
+ attaches = {
+ 'videofile': join(__dirname, '..', '..', 'fixtures', 'video_short.mkv')
+ }
+ await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
})
it('Should fail with an incorrect thumbnail file', async function () {
setAccessTokensToServers,
updateCustomConfig
} from '../../../../shared/utils'
+import { ServerConfig } from '../../../../shared/models'
const expect = chai.expect
expect(data.user.videoQuota).to.equal(5242880)
expect(data.user.videoQuotaDaily).to.equal(-1)
expect(data.transcoding.enabled).to.be.false
+ expect(data.transcoding.allowAdditionalExtensions).to.be.false
expect(data.transcoding.threads).to.equal(2)
expect(data.transcoding.resolutions['240p']).to.be.true
expect(data.transcoding.resolutions['360p']).to.be.true
expect(data.user.videoQuotaDaily).to.equal(318742)
expect(data.transcoding.enabled).to.be.true
expect(data.transcoding.threads).to.equal(1)
+ expect(data.transcoding.allowAdditionalExtensions).to.be.true
expect(data.transcoding.resolutions['240p']).to.be.false
expect(data.transcoding.resolutions['360p']).to.be.true
expect(data.transcoding.resolutions['480p']).to.be.true
it('Should have a correct config on a server with registration enabled', async function () {
const res = await getConfig(server.url)
- const data = res.body
+ const data: ServerConfig = res.body
expect(data.signup.allowed).to.be.true
})
])
const res = await getConfig(server.url)
- const data = res.body
+ const data: ServerConfig = res.body
expect(data.signup.allowed).to.be.false
})
+ it('Should have the correct video allowed extensions', async function () {
+ const res = await getConfig(server.url)
+ const data: ServerConfig = res.body
+
+ expect(data.video.file.extensions).to.have.lengthOf(3)
+ expect(data.video.file.extensions).to.contain('.mp4')
+ expect(data.video.file.extensions).to.contain('.webm')
+ expect(data.video.file.extensions).to.contain('.ogv')
+ })
+
it('Should get the customized configuration', async function () {
const res = await getCustomConfig(server.url, server.accessToken)
const data = res.body as CustomConfig
},
transcoding: {
enabled: true,
+ allowAdditionalExtensions: true,
threads: 1,
resolutions: {
'240p': false,
checkUpdatedConfig(data)
})
+ it('Should have the correct updated video allowed extensions', async function () {
+ const res = await getConfig(server.url)
+ const data: ServerConfig = res.body
+
+ expect(data.video.file.extensions).to.have.length.above(3)
+ expect(data.video.file.extensions).to.contain('.mp4')
+ expect(data.video.file.extensions).to.contain('.webm')
+ expect(data.video.file.extensions).to.contain('.ogv')
+ expect(data.video.file.extensions).to.contain('.flv')
+ expect(data.video.file.extensions).to.contain('.mkv')
+ })
+
it('Should have the configuration updated after a restart', async function () {
this.timeout(10000)
uploadVideo,
webtorrentAdd
} from '../../../../shared/utils'
-import { join } from 'path'
+import { extname, join } from 'path'
import { waitJobs } from '../../../../shared/utils/server/jobs'
-import { pathExists } from 'fs-extra'
import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants'
const expect = chai.expect
}
})
+ it('Should accept and transcode additional extensions', async function () {
+ this.timeout(300000)
+
+ for (const fixture of [ 'video_short.mkv', 'video_short.avi' ]) {
+ const videoAttributes = {
+ name: fixture,
+ fixture
+ }
+
+ await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, videoAttributes)
+
+ await waitJobs(servers)
+
+ for (const server of servers) {
+ const res = await getVideosList(server.url)
+
+ const video = res.body.data.find(v => v.name === videoAttributes.name)
+ const res2 = await getVideo(server.url, video.id)
+ const videoDetails = res2.body
+
+ expect(videoDetails.files).to.have.lengthOf(4)
+
+ const magnetUri = videoDetails.files[ 0 ].magnetUri
+ expect(magnetUri).to.contain('.mp4')
+ }
+ }
+ })
+
after(async function () {
killallServers(servers)
})
transcoding: {
enabled: boolean
+ allowAdditionalExtensions: boolean
threads: number
resolutions: {
'240p': boolean
},
transcoding: {
enabled: true,
+ allowAdditionalExtensions: true,
threads: 1,
resolutions: {
'240p': false,