--- /dev/null
+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
+}
--- /dev/null
+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
+}
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,
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'
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 {
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'
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[] = [
}
}
- 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'
}
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 () {
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())
.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