Reduce video.ts file size by moving some methods in other files
This commit is contained in:
parent
df182b373f
commit
098eb37797
7 changed files with 455 additions and 403 deletions
|
@ -32,7 +32,7 @@ redundancy:
|
|||
-
|
||||
size: '10MB'
|
||||
strategy: 'recently-added'
|
||||
minViews: 10
|
||||
minViews: 1
|
||||
|
||||
cache:
|
||||
previews:
|
||||
|
|
|
@ -8,6 +8,7 @@ import { VideoModel } from '../models/video/video'
|
|||
import * as validator from 'validator'
|
||||
import { VideoPrivacy } from '../../shared/models/videos'
|
||||
import { readFile } from 'fs-extra'
|
||||
import { getActivityStreamDuration } from '../models/video/video-format-utils'
|
||||
|
||||
export class ClientHtml {
|
||||
|
||||
|
@ -150,7 +151,7 @@ export class ClientHtml {
|
|||
description: videoDescriptionEscaped,
|
||||
thumbnailUrl: previewUrl,
|
||||
uploadDate: video.createdAt.toISOString(),
|
||||
duration: video.getActivityStreamDuration(),
|
||||
duration: getActivityStreamDuration(video.duration),
|
||||
contentUrl: videoUrl,
|
||||
embedUrl: embedUrl,
|
||||
interactionCount: video.views
|
||||
|
|
|
@ -8,6 +8,7 @@ import { retryTransactionWrapper } from '../../../helpers/database-utils'
|
|||
import { sequelizeTypescript } from '../../../initializers'
|
||||
import * as Bluebird from 'bluebird'
|
||||
import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils'
|
||||
import { importVideoFile, transcodeOriginalVideofile, optimizeOriginalVideofile } from '../../video-transcoding'
|
||||
|
||||
export type VideoFilePayload = {
|
||||
videoUUID: string
|
||||
|
@ -32,7 +33,7 @@ async function processVideoFileImport (job: Bull.Job) {
|
|||
return undefined
|
||||
}
|
||||
|
||||
await video.importVideoFile(payload.filePath)
|
||||
await importVideoFile(video, payload.filePath)
|
||||
|
||||
await onVideoFileTranscoderOrImportSuccess(video)
|
||||
return video
|
||||
|
@ -51,11 +52,11 @@ async function processVideoFile (job: Bull.Job) {
|
|||
|
||||
// Transcoding in other resolution
|
||||
if (payload.resolution) {
|
||||
await video.transcodeOriginalVideofile(payload.resolution, payload.isPortraitMode || false)
|
||||
await transcodeOriginalVideofile(video, payload.resolution, payload.isPortraitMode || false)
|
||||
|
||||
await retryTransactionWrapper(onVideoFileTranscoderOrImportSuccess, video)
|
||||
} else {
|
||||
await video.optimizeOriginalVideofile()
|
||||
await optimizeOriginalVideofile(video)
|
||||
|
||||
await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload.isNewVideo)
|
||||
}
|
||||
|
|
130
server/lib/video-transcoding.ts
Normal file
130
server/lib/video-transcoding.ts
Normal file
|
@ -0,0 +1,130 @@
|
|||
import { CONFIG } from '../initializers'
|
||||
import { join, extname } from 'path'
|
||||
import { getVideoFileFPS, getVideoFileResolution, transcode } from '../helpers/ffmpeg-utils'
|
||||
import { copy, remove, rename, stat } from 'fs-extra'
|
||||
import { logger } from '../helpers/logger'
|
||||
import { VideoResolution } from '../../shared/models/videos'
|
||||
import { VideoFileModel } from '../models/video/video-file'
|
||||
import { VideoModel } from '../models/video/video'
|
||||
|
||||
async function optimizeOriginalVideofile (video: VideoModel) {
|
||||
const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
|
||||
const newExtname = '.mp4'
|
||||
const inputVideoFile = video.getOriginalFile()
|
||||
const videoInputPath = join(videosDirectory, video.getVideoFilename(inputVideoFile))
|
||||
const videoTranscodedPath = join(videosDirectory, video.id + '-transcoded' + newExtname)
|
||||
|
||||
const transcodeOptions = {
|
||||
inputPath: videoInputPath,
|
||||
outputPath: videoTranscodedPath
|
||||
}
|
||||
|
||||
// Could be very long!
|
||||
await transcode(transcodeOptions)
|
||||
|
||||
try {
|
||||
await remove(videoInputPath)
|
||||
|
||||
// Important to do this before getVideoFilename() to take in account the new file extension
|
||||
inputVideoFile.set('extname', newExtname)
|
||||
|
||||
const videoOutputPath = video.getVideoFilePath(inputVideoFile)
|
||||
await rename(videoTranscodedPath, videoOutputPath)
|
||||
const stats = await stat(videoOutputPath)
|
||||
const fps = await getVideoFileFPS(videoOutputPath)
|
||||
|
||||
inputVideoFile.set('size', stats.size)
|
||||
inputVideoFile.set('fps', fps)
|
||||
|
||||
await video.createTorrentAndSetInfoHash(inputVideoFile)
|
||||
await inputVideoFile.save()
|
||||
} catch (err) {
|
||||
// Auto destruction...
|
||||
video.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', { err }))
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoResolution, isPortraitMode: boolean) {
|
||||
const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
|
||||
const extname = '.mp4'
|
||||
|
||||
// We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
|
||||
const videoInputPath = join(videosDirectory, video.getVideoFilename(video.getOriginalFile()))
|
||||
|
||||
const newVideoFile = new VideoFileModel({
|
||||
resolution,
|
||||
extname,
|
||||
size: 0,
|
||||
videoId: video.id
|
||||
})
|
||||
const videoOutputPath = join(videosDirectory, video.getVideoFilename(newVideoFile))
|
||||
|
||||
const transcodeOptions = {
|
||||
inputPath: videoInputPath,
|
||||
outputPath: videoOutputPath,
|
||||
resolution,
|
||||
isPortraitMode
|
||||
}
|
||||
|
||||
await transcode(transcodeOptions)
|
||||
|
||||
const stats = await stat(videoOutputPath)
|
||||
const fps = await getVideoFileFPS(videoOutputPath)
|
||||
|
||||
newVideoFile.set('size', stats.size)
|
||||
newVideoFile.set('fps', fps)
|
||||
|
||||
await video.createTorrentAndSetInfoHash(newVideoFile)
|
||||
|
||||
await newVideoFile.save()
|
||||
|
||||
video.VideoFiles.push(newVideoFile)
|
||||
}
|
||||
|
||||
async function importVideoFile (video: VideoModel, inputFilePath: string) {
|
||||
const { videoFileResolution } = await getVideoFileResolution(inputFilePath)
|
||||
const { size } = await stat(inputFilePath)
|
||||
const fps = await getVideoFileFPS(inputFilePath)
|
||||
|
||||
let updatedVideoFile = new VideoFileModel({
|
||||
resolution: videoFileResolution,
|
||||
extname: extname(inputFilePath),
|
||||
size,
|
||||
fps,
|
||||
videoId: video.id
|
||||
})
|
||||
|
||||
const currentVideoFile = video.VideoFiles.find(videoFile => videoFile.resolution === updatedVideoFile.resolution)
|
||||
|
||||
if (currentVideoFile) {
|
||||
// Remove old file and old torrent
|
||||
await video.removeFile(currentVideoFile)
|
||||
await video.removeTorrent(currentVideoFile)
|
||||
// Remove the old video file from the array
|
||||
video.VideoFiles = video.VideoFiles.filter(f => f !== currentVideoFile)
|
||||
|
||||
// Update the database
|
||||
currentVideoFile.set('extname', updatedVideoFile.extname)
|
||||
currentVideoFile.set('size', updatedVideoFile.size)
|
||||
currentVideoFile.set('fps', updatedVideoFile.fps)
|
||||
|
||||
updatedVideoFile = currentVideoFile
|
||||
}
|
||||
|
||||
const outputPath = video.getVideoFilePath(updatedVideoFile)
|
||||
await copy(inputFilePath, outputPath)
|
||||
|
||||
await video.createTorrentAndSetInfoHash(updatedVideoFile)
|
||||
|
||||
await updatedVideoFile.save()
|
||||
|
||||
video.VideoFiles.push(updatedVideoFile)
|
||||
}
|
||||
|
||||
export {
|
||||
optimizeOriginalVideofile,
|
||||
transcodeOriginalVideofile,
|
||||
importVideoFile
|
||||
}
|
|
@ -193,7 +193,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
|
|||
// On VideoModel!
|
||||
const query = {
|
||||
attributes: [ 'id', 'publishedAt' ],
|
||||
// logging: !isTestInstance(),
|
||||
logging: !isTestInstance(),
|
||||
limit: randomizedFactor,
|
||||
order: getVideoSort('-publishedAt'),
|
||||
where: {
|
||||
|
|
295
server/models/video/video-format-utils.ts
Normal file
295
server/models/video/video-format-utils.ts
Normal file
|
@ -0,0 +1,295 @@
|
|||
import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
|
||||
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 { VideoCaptionModel } from './video-caption'
|
||||
import {
|
||||
getVideoCommentsActivityPubUrl,
|
||||
getVideoDislikesActivityPubUrl,
|
||||
getVideoLikesActivityPubUrl,
|
||||
getVideoSharesActivityPubUrl
|
||||
} from '../../lib/activitypub'
|
||||
|
||||
export type VideoFormattingJSONOptions = {
|
||||
additionalAttributes: {
|
||||
state?: boolean,
|
||||
waitTranscoding?: boolean,
|
||||
scheduledUpdate?: boolean,
|
||||
blacklistInfo?: boolean
|
||||
}
|
||||
}
|
||||
function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormattingJSONOptions): Video {
|
||||
const formattedAccount = video.VideoChannel.Account.toFormattedJSON()
|
||||
const formattedVideoChannel = video.VideoChannel.toFormattedJSON()
|
||||
|
||||
const videoObject: Video = {
|
||||
id: video.id,
|
||||
uuid: video.uuid,
|
||||
name: video.name,
|
||||
category: {
|
||||
id: video.category,
|
||||
label: VideoModel.getCategoryLabel(video.category)
|
||||
},
|
||||
licence: {
|
||||
id: video.licence,
|
||||
label: VideoModel.getLicenceLabel(video.licence)
|
||||
},
|
||||
language: {
|
||||
id: video.language,
|
||||
label: VideoModel.getLanguageLabel(video.language)
|
||||
},
|
||||
privacy: {
|
||||
id: video.privacy,
|
||||
label: VideoModel.getPrivacyLabel(video.privacy)
|
||||
},
|
||||
nsfw: video.nsfw,
|
||||
description: video.getTruncatedDescription(),
|
||||
isLocal: video.isOwned(),
|
||||
duration: video.duration,
|
||||
views: video.views,
|
||||
likes: video.likes,
|
||||
dislikes: video.dislikes,
|
||||
thumbnailPath: video.getThumbnailStaticPath(),
|
||||
previewPath: video.getPreviewStaticPath(),
|
||||
embedPath: video.getEmbedStaticPath(),
|
||||
createdAt: video.createdAt,
|
||||
updatedAt: video.updatedAt,
|
||||
publishedAt: video.publishedAt,
|
||||
account: {
|
||||
id: formattedAccount.id,
|
||||
uuid: formattedAccount.uuid,
|
||||
name: formattedAccount.name,
|
||||
displayName: formattedAccount.displayName,
|
||||
url: formattedAccount.url,
|
||||
host: formattedAccount.host,
|
||||
avatar: formattedAccount.avatar
|
||||
},
|
||||
channel: {
|
||||
id: formattedVideoChannel.id,
|
||||
uuid: formattedVideoChannel.uuid,
|
||||
name: formattedVideoChannel.name,
|
||||
displayName: formattedVideoChannel.displayName,
|
||||
url: formattedVideoChannel.url,
|
||||
host: formattedVideoChannel.host,
|
||||
avatar: formattedVideoChannel.avatar
|
||||
}
|
||||
}
|
||||
|
||||
if (options) {
|
||||
if (options.additionalAttributes.state === true) {
|
||||
videoObject.state = {
|
||||
id: video.state,
|
||||
label: VideoModel.getStateLabel(video.state)
|
||||
}
|
||||
}
|
||||
|
||||
if (options.additionalAttributes.waitTranscoding === true) {
|
||||
videoObject.waitTranscoding = video.waitTranscoding
|
||||
}
|
||||
|
||||
if (options.additionalAttributes.scheduledUpdate === true && video.ScheduleVideoUpdate) {
|
||||
videoObject.scheduledUpdate = {
|
||||
updateAt: video.ScheduleVideoUpdate.updateAt,
|
||||
privacy: video.ScheduleVideoUpdate.privacy || undefined
|
||||
}
|
||||
}
|
||||
|
||||
if (options.additionalAttributes.blacklistInfo === true) {
|
||||
videoObject.blacklisted = !!video.VideoBlacklist
|
||||
videoObject.blacklistedReason = video.VideoBlacklist ? video.VideoBlacklist.reason : null
|
||||
}
|
||||
}
|
||||
|
||||
return videoObject
|
||||
}
|
||||
|
||||
function videoModelToFormattedDetailsJSON (video: VideoModel): VideoDetails {
|
||||
const formattedJson = video.toFormattedJSON({
|
||||
additionalAttributes: {
|
||||
scheduledUpdate: true,
|
||||
blacklistInfo: true
|
||||
}
|
||||
})
|
||||
|
||||
const detailsJson = {
|
||||
support: video.support,
|
||||
descriptionPath: video.getDescriptionPath(),
|
||||
channel: video.VideoChannel.toFormattedJSON(),
|
||||
account: video.VideoChannel.Account.toFormattedJSON(),
|
||||
tags: video.Tags.map(t => t.name),
|
||||
commentsEnabled: video.commentsEnabled,
|
||||
waitTranscoding: video.waitTranscoding,
|
||||
state: {
|
||||
id: video.state,
|
||||
label: VideoModel.getStateLabel(video.state)
|
||||
},
|
||||
files: []
|
||||
}
|
||||
|
||||
// Format and sort video files
|
||||
detailsJson.files = videoFilesModelToFormattedJSON(video, video.VideoFiles)
|
||||
|
||||
return Object.assign(formattedJson, detailsJson)
|
||||
}
|
||||
|
||||
function videoFilesModelToFormattedJSON (video: VideoModel, videoFiles: VideoFileModel[]): VideoFile[] {
|
||||
const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
|
||||
|
||||
return videoFiles
|
||||
.map(videoFile => {
|
||||
let resolutionLabel = videoFile.resolution + 'p'
|
||||
|
||||
return {
|
||||
resolution: {
|
||||
id: videoFile.resolution,
|
||||
label: resolutionLabel
|
||||
},
|
||||
magnetUri: video.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs),
|
||||
size: videoFile.size,
|
||||
fps: videoFile.fps,
|
||||
torrentUrl: video.getTorrentUrl(videoFile, baseUrlHttp),
|
||||
torrentDownloadUrl: video.getTorrentDownloadUrl(videoFile, baseUrlHttp),
|
||||
fileUrl: video.getVideoFileUrl(videoFile, baseUrlHttp),
|
||||
fileDownloadUrl: video.getVideoFileDownloadUrl(videoFile, baseUrlHttp)
|
||||
} as VideoFile
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.resolution.id < b.resolution.id) return 1
|
||||
if (a.resolution.id === b.resolution.id) return 0
|
||||
return -1
|
||||
})
|
||||
}
|
||||
|
||||
function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject {
|
||||
const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
|
||||
if (!video.Tags) video.Tags = []
|
||||
|
||||
const tag = video.Tags.map(t => ({
|
||||
type: 'Hashtag' as 'Hashtag',
|
||||
name: t.name
|
||||
}))
|
||||
|
||||
let language
|
||||
if (video.language) {
|
||||
language = {
|
||||
identifier: video.language,
|
||||
name: VideoModel.getLanguageLabel(video.language)
|
||||
}
|
||||
}
|
||||
|
||||
let category
|
||||
if (video.category) {
|
||||
category = {
|
||||
identifier: video.category + '',
|
||||
name: VideoModel.getCategoryLabel(video.category)
|
||||
}
|
||||
}
|
||||
|
||||
let licence
|
||||
if (video.licence) {
|
||||
licence = {
|
||||
identifier: video.licence + '',
|
||||
name: VideoModel.getLicenceLabel(video.licence)
|
||||
}
|
||||
}
|
||||
|
||||
const url: ActivityUrlObject[] = []
|
||||
for (const file of video.VideoFiles) {
|
||||
url.push({
|
||||
type: 'Link',
|
||||
mimeType: VIDEO_EXT_MIMETYPE[ file.extname ] as any,
|
||||
href: video.getVideoFileUrl(file, baseUrlHttp),
|
||||
height: file.resolution,
|
||||
size: file.size,
|
||||
fps: file.fps
|
||||
})
|
||||
|
||||
url.push({
|
||||
type: 'Link',
|
||||
mimeType: 'application/x-bittorrent' as 'application/x-bittorrent',
|
||||
href: video.getTorrentUrl(file, baseUrlHttp),
|
||||
height: file.resolution
|
||||
})
|
||||
|
||||
url.push({
|
||||
type: 'Link',
|
||||
mimeType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
|
||||
href: video.generateMagnetUri(file, baseUrlHttp, baseUrlWs),
|
||||
height: file.resolution
|
||||
})
|
||||
}
|
||||
|
||||
// Add video url too
|
||||
url.push({
|
||||
type: 'Link',
|
||||
mimeType: 'text/html',
|
||||
href: CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid
|
||||
})
|
||||
|
||||
const subtitleLanguage = []
|
||||
for (const caption of video.VideoCaptions) {
|
||||
subtitleLanguage.push({
|
||||
identifier: caption.language,
|
||||
name: VideoCaptionModel.getLanguageLabel(caption.language)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'Video' as 'Video',
|
||||
id: video.url,
|
||||
name: video.name,
|
||||
duration: getActivityStreamDuration(video.duration),
|
||||
uuid: video.uuid,
|
||||
tag,
|
||||
category,
|
||||
licence,
|
||||
language,
|
||||
views: video.views,
|
||||
sensitive: video.nsfw,
|
||||
waitTranscoding: video.waitTranscoding,
|
||||
state: video.state,
|
||||
commentsEnabled: video.commentsEnabled,
|
||||
published: video.publishedAt.toISOString(),
|
||||
updated: video.updatedAt.toISOString(),
|
||||
mediaType: 'text/markdown',
|
||||
content: video.getTruncatedDescription(),
|
||||
support: video.support,
|
||||
subtitleLanguage,
|
||||
icon: {
|
||||
type: 'Image',
|
||||
url: video.getThumbnailUrl(baseUrlHttp),
|
||||
mediaType: 'image/jpeg',
|
||||
width: THUMBNAILS_SIZE.width,
|
||||
height: THUMBNAILS_SIZE.height
|
||||
},
|
||||
url,
|
||||
likes: getVideoLikesActivityPubUrl(video),
|
||||
dislikes: getVideoDislikesActivityPubUrl(video),
|
||||
shares: getVideoSharesActivityPubUrl(video),
|
||||
comments: getVideoCommentsActivityPubUrl(video),
|
||||
attributedTo: [
|
||||
{
|
||||
type: 'Person',
|
||||
id: video.VideoChannel.Account.Actor.url
|
||||
},
|
||||
{
|
||||
type: 'Group',
|
||||
id: video.VideoChannel.Actor.url
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
function getActivityStreamDuration (duration: number) {
|
||||
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
|
||||
return 'PT' + duration + 'S'
|
||||
}
|
||||
|
||||
export {
|
||||
videoModelToFormattedJSON,
|
||||
videoModelToFormattedDetailsJSON,
|
||||
videoFilesModelToFormattedJSON,
|
||||
videoModelToActivityPubObject,
|
||||
getActivityStreamDuration
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
import * as Bluebird from 'bluebird'
|
||||
import { map, maxBy } from 'lodash'
|
||||
import { maxBy } from 'lodash'
|
||||
import * as magnetUtil from 'magnet-uri'
|
||||
import * as parseTorrent from 'parse-torrent'
|
||||
import { extname, join } from 'path'
|
||||
import { join } from 'path'
|
||||
import * as Sequelize from 'sequelize'
|
||||
import {
|
||||
AllowNull,
|
||||
|
@ -27,7 +27,7 @@ import {
|
|||
Table,
|
||||
UpdatedAt
|
||||
} from 'sequelize-typescript'
|
||||
import { ActivityUrlObject, VideoPrivacy, VideoResolution, VideoState } from '../../../shared'
|
||||
import { VideoPrivacy, VideoState } from '../../../shared'
|
||||
import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
|
||||
import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
|
||||
import { VideoFilter } from '../../../shared/models/videos/video-query.type'
|
||||
|
@ -45,7 +45,7 @@ import {
|
|||
isVideoStateValid,
|
||||
isVideoSupportValid
|
||||
} from '../../helpers/custom-validators/videos'
|
||||
import { generateImageFromVideoFile, getVideoFileFPS, getVideoFileResolution, transcode } from '../../helpers/ffmpeg-utils'
|
||||
import { generateImageFromVideoFile, getVideoFileResolution } from '../../helpers/ffmpeg-utils'
|
||||
import { logger } from '../../helpers/logger'
|
||||
import { getServerActor } from '../../helpers/utils'
|
||||
import {
|
||||
|
@ -59,18 +59,11 @@ import {
|
|||
STATIC_PATHS,
|
||||
THUMBNAILS_SIZE,
|
||||
VIDEO_CATEGORIES,
|
||||
VIDEO_EXT_MIMETYPE,
|
||||
VIDEO_LANGUAGES,
|
||||
VIDEO_LICENCES,
|
||||
VIDEO_PRIVACIES,
|
||||
VIDEO_STATES
|
||||
} from '../../initializers'
|
||||
import {
|
||||
getVideoCommentsActivityPubUrl,
|
||||
getVideoDislikesActivityPubUrl,
|
||||
getVideoLikesActivityPubUrl,
|
||||
getVideoSharesActivityPubUrl
|
||||
} from '../../lib/activitypub'
|
||||
import { sendDeleteVideo } from '../../lib/activitypub/send'
|
||||
import { AccountModel } from '../account/account'
|
||||
import { AccountVideoRateModel } from '../account/account-video-rate'
|
||||
|
@ -88,9 +81,16 @@ import { VideoTagModel } from './video-tag'
|
|||
import { ScheduleVideoUpdateModel } from './schedule-video-update'
|
||||
import { VideoCaptionModel } from './video-caption'
|
||||
import { VideoBlacklistModel } from './video-blacklist'
|
||||
import { copy, remove, rename, stat, writeFile } from 'fs-extra'
|
||||
import { remove, writeFile } from 'fs-extra'
|
||||
import { VideoViewModel } from './video-views'
|
||||
import { VideoRedundancyModel } from '../redundancy/video-redundancy'
|
||||
import {
|
||||
videoFilesModelToFormattedJSON,
|
||||
VideoFormattingJSONOptions,
|
||||
videoModelToActivityPubObject,
|
||||
videoModelToFormattedDetailsJSON,
|
||||
videoModelToFormattedJSON
|
||||
} from './video-format-utils'
|
||||
|
||||
// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
|
||||
const indexes: Sequelize.DefineIndexesOptions[] = [
|
||||
|
@ -1257,23 +1257,23 @@ export class VideoModel extends Model<VideoModel> {
|
|||
}
|
||||
}
|
||||
|
||||
private static getCategoryLabel (id: number) {
|
||||
static getCategoryLabel (id: number) {
|
||||
return VIDEO_CATEGORIES[ id ] || 'Misc'
|
||||
}
|
||||
|
||||
private static getLicenceLabel (id: number) {
|
||||
static getLicenceLabel (id: number) {
|
||||
return VIDEO_LICENCES[ id ] || 'Unknown'
|
||||
}
|
||||
|
||||
private static getLanguageLabel (id: string) {
|
||||
static getLanguageLabel (id: string) {
|
||||
return VIDEO_LANGUAGES[ id ] || 'Unknown'
|
||||
}
|
||||
|
||||
private static getPrivacyLabel (id: number) {
|
||||
static getPrivacyLabel (id: number) {
|
||||
return VIDEO_PRIVACIES[ id ] || 'Unknown'
|
||||
}
|
||||
|
||||
private static getStateLabel (id: number) {
|
||||
static getStateLabel (id: number) {
|
||||
return VIDEO_STATES[ id ] || 'Unknown'
|
||||
}
|
||||
|
||||
|
@ -1369,273 +1369,20 @@ export class VideoModel extends Model<VideoModel> {
|
|||
return join(STATIC_PATHS.PREVIEWS, this.getPreviewName())
|
||||
}
|
||||
|
||||
toFormattedJSON (options?: {
|
||||
additionalAttributes: {
|
||||
state?: boolean,
|
||||
waitTranscoding?: boolean,
|
||||
scheduledUpdate?: boolean,
|
||||
blacklistInfo?: boolean
|
||||
}
|
||||
}): Video {
|
||||
const formattedAccount = this.VideoChannel.Account.toFormattedJSON()
|
||||
const formattedVideoChannel = this.VideoChannel.toFormattedJSON()
|
||||
|
||||
const videoObject: Video = {
|
||||
id: this.id,
|
||||
uuid: this.uuid,
|
||||
name: this.name,
|
||||
category: {
|
||||
id: this.category,
|
||||
label: VideoModel.getCategoryLabel(this.category)
|
||||
},
|
||||
licence: {
|
||||
id: this.licence,
|
||||
label: VideoModel.getLicenceLabel(this.licence)
|
||||
},
|
||||
language: {
|
||||
id: this.language,
|
||||
label: VideoModel.getLanguageLabel(this.language)
|
||||
},
|
||||
privacy: {
|
||||
id: this.privacy,
|
||||
label: VideoModel.getPrivacyLabel(this.privacy)
|
||||
},
|
||||
nsfw: this.nsfw,
|
||||
description: this.getTruncatedDescription(),
|
||||
isLocal: this.isOwned(),
|
||||
duration: this.duration,
|
||||
views: this.views,
|
||||
likes: this.likes,
|
||||
dislikes: this.dislikes,
|
||||
thumbnailPath: this.getThumbnailStaticPath(),
|
||||
previewPath: this.getPreviewStaticPath(),
|
||||
embedPath: this.getEmbedStaticPath(),
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt,
|
||||
publishedAt: this.publishedAt,
|
||||
account: {
|
||||
id: formattedAccount.id,
|
||||
uuid: formattedAccount.uuid,
|
||||
name: formattedAccount.name,
|
||||
displayName: formattedAccount.displayName,
|
||||
url: formattedAccount.url,
|
||||
host: formattedAccount.host,
|
||||
avatar: formattedAccount.avatar
|
||||
},
|
||||
channel: {
|
||||
id: formattedVideoChannel.id,
|
||||
uuid: formattedVideoChannel.uuid,
|
||||
name: formattedVideoChannel.name,
|
||||
displayName: formattedVideoChannel.displayName,
|
||||
url: formattedVideoChannel.url,
|
||||
host: formattedVideoChannel.host,
|
||||
avatar: formattedVideoChannel.avatar
|
||||
}
|
||||
}
|
||||
|
||||
if (options) {
|
||||
if (options.additionalAttributes.state === true) {
|
||||
videoObject.state = {
|
||||
id: this.state,
|
||||
label: VideoModel.getStateLabel(this.state)
|
||||
}
|
||||
}
|
||||
|
||||
if (options.additionalAttributes.waitTranscoding === true) {
|
||||
videoObject.waitTranscoding = this.waitTranscoding
|
||||
}
|
||||
|
||||
if (options.additionalAttributes.scheduledUpdate === true && this.ScheduleVideoUpdate) {
|
||||
videoObject.scheduledUpdate = {
|
||||
updateAt: this.ScheduleVideoUpdate.updateAt,
|
||||
privacy: this.ScheduleVideoUpdate.privacy || undefined
|
||||
}
|
||||
}
|
||||
|
||||
if (options.additionalAttributes.blacklistInfo === true) {
|
||||
videoObject.blacklisted = !!this.VideoBlacklist
|
||||
videoObject.blacklistedReason = this.VideoBlacklist ? this.VideoBlacklist.reason : null
|
||||
}
|
||||
}
|
||||
|
||||
return videoObject
|
||||
toFormattedJSON (options?: VideoFormattingJSONOptions): Video {
|
||||
return videoModelToFormattedJSON(this, options)
|
||||
}
|
||||
|
||||
toFormattedDetailsJSON (): VideoDetails {
|
||||
const formattedJson = this.toFormattedJSON({
|
||||
additionalAttributes: {
|
||||
scheduledUpdate: true,
|
||||
blacklistInfo: true
|
||||
}
|
||||
})
|
||||
|
||||
const detailsJson = {
|
||||
support: this.support,
|
||||
descriptionPath: this.getDescriptionPath(),
|
||||
channel: this.VideoChannel.toFormattedJSON(),
|
||||
account: this.VideoChannel.Account.toFormattedJSON(),
|
||||
tags: map(this.Tags, 'name'),
|
||||
commentsEnabled: this.commentsEnabled,
|
||||
waitTranscoding: this.waitTranscoding,
|
||||
state: {
|
||||
id: this.state,
|
||||
label: VideoModel.getStateLabel(this.state)
|
||||
},
|
||||
files: []
|
||||
}
|
||||
|
||||
// Format and sort video files
|
||||
detailsJson.files = this.getFormattedVideoFilesJSON()
|
||||
|
||||
return Object.assign(formattedJson, detailsJson)
|
||||
return videoModelToFormattedDetailsJSON(this)
|
||||
}
|
||||
|
||||
getFormattedVideoFilesJSON (): VideoFile[] {
|
||||
const { baseUrlHttp, baseUrlWs } = this.getBaseUrls()
|
||||
|
||||
return this.VideoFiles
|
||||
.map(videoFile => {
|
||||
let resolutionLabel = videoFile.resolution + 'p'
|
||||
|
||||
return {
|
||||
resolution: {
|
||||
id: videoFile.resolution,
|
||||
label: resolutionLabel
|
||||
},
|
||||
magnetUri: this.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs),
|
||||
size: videoFile.size,
|
||||
fps: videoFile.fps,
|
||||
torrentUrl: this.getTorrentUrl(videoFile, baseUrlHttp),
|
||||
torrentDownloadUrl: this.getTorrentDownloadUrl(videoFile, baseUrlHttp),
|
||||
fileUrl: this.getVideoFileUrl(videoFile, baseUrlHttp),
|
||||
fileDownloadUrl: this.getVideoFileDownloadUrl(videoFile, baseUrlHttp)
|
||||
} as VideoFile
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.resolution.id < b.resolution.id) return 1
|
||||
if (a.resolution.id === b.resolution.id) return 0
|
||||
return -1
|
||||
})
|
||||
return videoFilesModelToFormattedJSON(this, this.VideoFiles)
|
||||
}
|
||||
|
||||
toActivityPubObject (): VideoTorrentObject {
|
||||
const { baseUrlHttp, baseUrlWs } = this.getBaseUrls()
|
||||
if (!this.Tags) this.Tags = []
|
||||
|
||||
const tag = this.Tags.map(t => ({
|
||||
type: 'Hashtag' as 'Hashtag',
|
||||
name: t.name
|
||||
}))
|
||||
|
||||
let language
|
||||
if (this.language) {
|
||||
language = {
|
||||
identifier: this.language,
|
||||
name: VideoModel.getLanguageLabel(this.language)
|
||||
}
|
||||
}
|
||||
|
||||
let category
|
||||
if (this.category) {
|
||||
category = {
|
||||
identifier: this.category + '',
|
||||
name: VideoModel.getCategoryLabel(this.category)
|
||||
}
|
||||
}
|
||||
|
||||
let licence
|
||||
if (this.licence) {
|
||||
licence = {
|
||||
identifier: this.licence + '',
|
||||
name: VideoModel.getLicenceLabel(this.licence)
|
||||
}
|
||||
}
|
||||
|
||||
const url: ActivityUrlObject[] = []
|
||||
for (const file of this.VideoFiles) {
|
||||
url.push({
|
||||
type: 'Link',
|
||||
mimeType: VIDEO_EXT_MIMETYPE[ file.extname ] as any,
|
||||
href: this.getVideoFileUrl(file, baseUrlHttp),
|
||||
height: file.resolution,
|
||||
size: file.size,
|
||||
fps: file.fps
|
||||
})
|
||||
|
||||
url.push({
|
||||
type: 'Link',
|
||||
mimeType: 'application/x-bittorrent' as 'application/x-bittorrent',
|
||||
href: this.getTorrentUrl(file, baseUrlHttp),
|
||||
height: file.resolution
|
||||
})
|
||||
|
||||
url.push({
|
||||
type: 'Link',
|
||||
mimeType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
|
||||
href: this.generateMagnetUri(file, baseUrlHttp, baseUrlWs),
|
||||
height: file.resolution
|
||||
})
|
||||
}
|
||||
|
||||
// Add video url too
|
||||
url.push({
|
||||
type: 'Link',
|
||||
mimeType: 'text/html',
|
||||
href: CONFIG.WEBSERVER.URL + '/videos/watch/' + this.uuid
|
||||
})
|
||||
|
||||
const subtitleLanguage = []
|
||||
for (const caption of this.VideoCaptions) {
|
||||
subtitleLanguage.push({
|
||||
identifier: caption.language,
|
||||
name: VideoCaptionModel.getLanguageLabel(caption.language)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'Video' as 'Video',
|
||||
id: this.url,
|
||||
name: this.name,
|
||||
duration: this.getActivityStreamDuration(),
|
||||
uuid: this.uuid,
|
||||
tag,
|
||||
category,
|
||||
licence,
|
||||
language,
|
||||
views: this.views,
|
||||
sensitive: this.nsfw,
|
||||
waitTranscoding: this.waitTranscoding,
|
||||
state: this.state,
|
||||
commentsEnabled: this.commentsEnabled,
|
||||
published: this.publishedAt.toISOString(),
|
||||
updated: this.updatedAt.toISOString(),
|
||||
mediaType: 'text/markdown',
|
||||
content: this.getTruncatedDescription(),
|
||||
support: this.support,
|
||||
subtitleLanguage,
|
||||
icon: {
|
||||
type: 'Image',
|
||||
url: this.getThumbnailUrl(baseUrlHttp),
|
||||
mediaType: 'image/jpeg',
|
||||
width: THUMBNAILS_SIZE.width,
|
||||
height: THUMBNAILS_SIZE.height
|
||||
},
|
||||
url,
|
||||
likes: getVideoLikesActivityPubUrl(this),
|
||||
dislikes: getVideoDislikesActivityPubUrl(this),
|
||||
shares: getVideoSharesActivityPubUrl(this),
|
||||
comments: getVideoCommentsActivityPubUrl(this),
|
||||
attributedTo: [
|
||||
{
|
||||
type: 'Person',
|
||||
id: this.VideoChannel.Account.Actor.url
|
||||
},
|
||||
{
|
||||
type: 'Group',
|
||||
id: this.VideoChannel.Actor.url
|
||||
}
|
||||
]
|
||||
}
|
||||
return videoModelToActivityPubObject(this)
|
||||
}
|
||||
|
||||
getTruncatedDescription () {
|
||||
|
@ -1645,123 +1392,6 @@ export class VideoModel extends Model<VideoModel> {
|
|||
return peertubeTruncate(this.description, maxLength)
|
||||
}
|
||||
|
||||
async optimizeOriginalVideofile () {
|
||||
const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
|
||||
const newExtname = '.mp4'
|
||||
const inputVideoFile = this.getOriginalFile()
|
||||
const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
|
||||
const videoTranscodedPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
|
||||
|
||||
const transcodeOptions = {
|
||||
inputPath: videoInputPath,
|
||||
outputPath: videoTranscodedPath
|
||||
}
|
||||
|
||||
// Could be very long!
|
||||
await transcode(transcodeOptions)
|
||||
|
||||
try {
|
||||
await remove(videoInputPath)
|
||||
|
||||
// Important to do this before getVideoFilename() to take in account the new file extension
|
||||
inputVideoFile.set('extname', newExtname)
|
||||
|
||||
const videoOutputPath = this.getVideoFilePath(inputVideoFile)
|
||||
await rename(videoTranscodedPath, videoOutputPath)
|
||||
const stats = await stat(videoOutputPath)
|
||||
const fps = await getVideoFileFPS(videoOutputPath)
|
||||
|
||||
inputVideoFile.set('size', stats.size)
|
||||
inputVideoFile.set('fps', fps)
|
||||
|
||||
await this.createTorrentAndSetInfoHash(inputVideoFile)
|
||||
await inputVideoFile.save()
|
||||
|
||||
} catch (err) {
|
||||
// Auto destruction...
|
||||
this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', { err }))
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async transcodeOriginalVideofile (resolution: VideoResolution, isPortraitMode: boolean) {
|
||||
const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
|
||||
const extname = '.mp4'
|
||||
|
||||
// We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
|
||||
const videoInputPath = join(videosDirectory, this.getVideoFilename(this.getOriginalFile()))
|
||||
|
||||
const newVideoFile = new VideoFileModel({
|
||||
resolution,
|
||||
extname,
|
||||
size: 0,
|
||||
videoId: this.id
|
||||
})
|
||||
const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile))
|
||||
|
||||
const transcodeOptions = {
|
||||
inputPath: videoInputPath,
|
||||
outputPath: videoOutputPath,
|
||||
resolution,
|
||||
isPortraitMode
|
||||
}
|
||||
|
||||
await transcode(transcodeOptions)
|
||||
|
||||
const stats = await stat(videoOutputPath)
|
||||
const fps = await getVideoFileFPS(videoOutputPath)
|
||||
|
||||
newVideoFile.set('size', stats.size)
|
||||
newVideoFile.set('fps', fps)
|
||||
|
||||
await this.createTorrentAndSetInfoHash(newVideoFile)
|
||||
|
||||
await newVideoFile.save()
|
||||
|
||||
this.VideoFiles.push(newVideoFile)
|
||||
}
|
||||
|
||||
async importVideoFile (inputFilePath: string) {
|
||||
const { videoFileResolution } = await getVideoFileResolution(inputFilePath)
|
||||
const { size } = await stat(inputFilePath)
|
||||
const fps = await getVideoFileFPS(inputFilePath)
|
||||
|
||||
let updatedVideoFile = new VideoFileModel({
|
||||
resolution: videoFileResolution,
|
||||
extname: extname(inputFilePath),
|
||||
size,
|
||||
fps,
|
||||
videoId: this.id
|
||||
})
|
||||
|
||||
const currentVideoFile = this.VideoFiles.find(videoFile => videoFile.resolution === updatedVideoFile.resolution)
|
||||
|
||||
if (currentVideoFile) {
|
||||
// Remove old file and old torrent
|
||||
await this.removeFile(currentVideoFile)
|
||||
await this.removeTorrent(currentVideoFile)
|
||||
// Remove the old video file from the array
|
||||
this.VideoFiles = this.VideoFiles.filter(f => f !== currentVideoFile)
|
||||
|
||||
// Update the database
|
||||
currentVideoFile.set('extname', updatedVideoFile.extname)
|
||||
currentVideoFile.set('size', updatedVideoFile.size)
|
||||
currentVideoFile.set('fps', updatedVideoFile.fps)
|
||||
|
||||
updatedVideoFile = currentVideoFile
|
||||
}
|
||||
|
||||
const outputPath = this.getVideoFilePath(updatedVideoFile)
|
||||
await copy(inputFilePath, outputPath)
|
||||
|
||||
await this.createTorrentAndSetInfoHash(updatedVideoFile)
|
||||
|
||||
await updatedVideoFile.save()
|
||||
|
||||
this.VideoFiles.push(updatedVideoFile)
|
||||
}
|
||||
|
||||
getOriginalFileResolution () {
|
||||
const originalFilePath = this.getVideoFilePath(this.getOriginalFile())
|
||||
|
||||
|
@ -1796,11 +1426,6 @@ export class VideoModel extends Model<VideoModel> {
|
|||
.catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
|
||||
}
|
||||
|
||||
getActivityStreamDuration () {
|
||||
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
|
||||
return 'PT' + this.duration + 'S'
|
||||
}
|
||||
|
||||
isOutdated () {
|
||||
if (this.isOwned()) return false
|
||||
|
||||
|
|
Loading…
Reference in a new issue