1
0
Fork 0

Reduce video.ts file size by moving some methods in other files

This commit is contained in:
Chocobozzz 2018-09-18 11:02:51 +02:00
parent df182b373f
commit 098eb37797
No known key found for this signature in database
GPG key ID: 583A612D890159BE
7 changed files with 455 additions and 403 deletions

View file

@ -32,7 +32,7 @@ redundancy:
-
size: '10MB'
strategy: 'recently-added'
minViews: 10
minViews: 1
cache:
previews:

View file

@ -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

View file

@ -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)
}

View 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
}

View file

@ -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: {

View 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
}

View file

@ -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