
* Separate player in a dedicated build, that we can control using vite. We had too many issues with Angular build system and we can now have the same build between the embed and the client. We can also embed SVG directly in the CSS * Upgrade p2p-media-loader to v2 * Update internal infohashes to reflect this p2p-media-loader protocol change (they are updated at PeerTube startup) * Minimum required iOS version is now v14
686 lines
18 KiB
TypeScript
686 lines
18 KiB
TypeScript
import {
|
|
ActivityVideoUrlObject,
|
|
FileStorage,
|
|
type FileStorageType,
|
|
VideoFileFormatFlag,
|
|
type VideoFileFormatFlagType,
|
|
VideoFileStream,
|
|
type VideoFileStreamType,
|
|
VideoResolution
|
|
} from '@peertube/peertube-models'
|
|
import { logger } from '@server/helpers/logger.js'
|
|
import { extractVideo } from '@server/helpers/video.js'
|
|
import { CONFIG } from '@server/initializers/config.js'
|
|
import { buildRemoteUrl } from '@server/lib/activitypub/url.js'
|
|
import {
|
|
getHLSPrivateFileUrl,
|
|
getObjectStoragePublicFileUrl,
|
|
getWebVideoPrivateFileUrl
|
|
} from '@server/lib/object-storage/index.js'
|
|
import { getFSTorrentFilePath } from '@server/lib/paths.js'
|
|
import { getVideoFileMimeType } from '@server/lib/video-file.js'
|
|
import { isVideoInPrivateDirectory } from '@server/lib/video-privacy.js'
|
|
import { MStreamingPlaylistVideo, MVideo, MVideoWithHost, isStreamingPlaylist } from '@server/types/models/index.js'
|
|
import { remove } from 'fs-extra/esm'
|
|
import memoizee from 'memoizee'
|
|
import { join } from 'path'
|
|
import { FindOptions, Op, Transaction, WhereOptions } from 'sequelize'
|
|
import {
|
|
AllowNull,
|
|
BelongsTo,
|
|
Column,
|
|
CreatedAt,
|
|
DataType,
|
|
Default,
|
|
DefaultScope,
|
|
ForeignKey,
|
|
Is, Scopes,
|
|
Table,
|
|
UpdatedAt
|
|
} from 'sequelize-typescript'
|
|
import validator from 'validator'
|
|
import {
|
|
isVideoFPSResolutionValid,
|
|
isVideoFileExtnameValid,
|
|
isVideoFileInfoHashValid,
|
|
isVideoFileResolutionValid,
|
|
isVideoFileSizeValid
|
|
} from '../../helpers/custom-validators/videos.js'
|
|
import {
|
|
DOWNLOAD_PATHS,
|
|
LAZY_STATIC_PATHS,
|
|
MEMOIZE_LENGTH,
|
|
MEMOIZE_TTL,
|
|
STATIC_PATHS,
|
|
WEBSERVER
|
|
} from '../../initializers/constants.js'
|
|
import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file.js'
|
|
import { SequelizeModel, doesExist, parseAggregateResult, throwIfNotValid } from '../shared/index.js'
|
|
import { VideoStreamingPlaylistModel } from './video-streaming-playlist.js'
|
|
import { VideoModel } from './video.js'
|
|
|
|
export enum ScopeNames {
|
|
WITH_VIDEO = 'WITH_VIDEO',
|
|
WITH_METADATA = 'WITH_METADATA',
|
|
WITH_VIDEO_OR_PLAYLIST = 'WITH_VIDEO_OR_PLAYLIST'
|
|
}
|
|
|
|
@DefaultScope(() => ({
|
|
attributes: {
|
|
exclude: [ 'metadata' ]
|
|
}
|
|
}))
|
|
@Scopes(() => ({
|
|
[ScopeNames.WITH_VIDEO]: {
|
|
include: [
|
|
{
|
|
model: VideoModel.unscoped(),
|
|
required: true
|
|
}
|
|
]
|
|
},
|
|
[ScopeNames.WITH_VIDEO_OR_PLAYLIST]: (options: { whereVideo?: WhereOptions } = {}) => {
|
|
return {
|
|
include: [
|
|
{
|
|
model: VideoModel.unscoped(),
|
|
required: false,
|
|
where: options.whereVideo
|
|
},
|
|
{
|
|
model: VideoStreamingPlaylistModel.unscoped(),
|
|
required: false,
|
|
include: [
|
|
{
|
|
model: VideoModel.unscoped(),
|
|
required: true,
|
|
where: options.whereVideo
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
},
|
|
[ScopeNames.WITH_METADATA]: {
|
|
attributes: {
|
|
include: [ 'metadata' ]
|
|
}
|
|
}
|
|
}))
|
|
@Table({
|
|
tableName: 'videoFile',
|
|
indexes: [
|
|
{
|
|
fields: [ 'infoHash' ]
|
|
},
|
|
|
|
{
|
|
fields: [ 'torrentFilename' ],
|
|
unique: true
|
|
},
|
|
|
|
{
|
|
fields: [ 'filename' ],
|
|
unique: true
|
|
},
|
|
|
|
{
|
|
fields: [ 'videoId', 'resolution', 'fps' ],
|
|
unique: true,
|
|
where: {
|
|
videoId: {
|
|
[Op.ne]: null
|
|
}
|
|
}
|
|
},
|
|
{
|
|
fields: [ 'videoStreamingPlaylistId', 'resolution', 'fps' ],
|
|
unique: true,
|
|
where: {
|
|
videoStreamingPlaylistId: {
|
|
[Op.ne]: null
|
|
}
|
|
}
|
|
}
|
|
]
|
|
})
|
|
export class VideoFileModel extends SequelizeModel<VideoFileModel> {
|
|
@CreatedAt
|
|
createdAt: Date
|
|
|
|
@UpdatedAt
|
|
updatedAt: Date
|
|
|
|
@AllowNull(false)
|
|
@Is('VideoFileResolution', value => throwIfNotValid(value, isVideoFileResolutionValid, 'resolution'))
|
|
@Column
|
|
resolution: number
|
|
|
|
@AllowNull(true)
|
|
@Column
|
|
width: number
|
|
|
|
@AllowNull(true)
|
|
@Column
|
|
height: number
|
|
|
|
@AllowNull(false)
|
|
@Is('VideoFileSize', value => throwIfNotValid(value, isVideoFileSizeValid, 'size'))
|
|
@Column(DataType.BIGINT)
|
|
size: number
|
|
|
|
@AllowNull(false)
|
|
@Is('VideoFileExtname', value => throwIfNotValid(value, isVideoFileExtnameValid, 'extname'))
|
|
@Column
|
|
extname: string
|
|
|
|
@AllowNull(true)
|
|
@Is('VideoFileInfohash', value => throwIfNotValid(value, isVideoFileInfoHashValid, 'info hash', true))
|
|
@Column
|
|
infoHash: string
|
|
|
|
@AllowNull(false)
|
|
@Default(-1)
|
|
@Is('VideoFileFPS', value => throwIfNotValid(value, isVideoFPSResolutionValid, 'fps'))
|
|
@Column
|
|
fps: number
|
|
|
|
@AllowNull(false)
|
|
@Column
|
|
formatFlags: VideoFileFormatFlagType
|
|
|
|
@AllowNull(false)
|
|
@Column
|
|
streams: VideoFileStreamType
|
|
|
|
@AllowNull(true)
|
|
@Column(DataType.JSONB)
|
|
metadata: any
|
|
|
|
@AllowNull(true)
|
|
@Column
|
|
metadataUrl: string
|
|
|
|
// Could be null for remote files
|
|
@AllowNull(true)
|
|
@Column
|
|
fileUrl: string
|
|
|
|
// Could be null for live files
|
|
@AllowNull(true)
|
|
@Column
|
|
filename: string
|
|
|
|
// Could be null for remote files
|
|
@AllowNull(true)
|
|
@Column
|
|
torrentUrl: string
|
|
|
|
// Could be null for live files
|
|
@AllowNull(true)
|
|
@Column
|
|
torrentFilename: string
|
|
|
|
@ForeignKey(() => VideoModel)
|
|
@Column
|
|
videoId: number
|
|
|
|
@AllowNull(false)
|
|
@Default(FileStorage.FILE_SYSTEM)
|
|
@Column
|
|
storage: FileStorageType
|
|
|
|
@BelongsTo(() => VideoModel, {
|
|
foreignKey: {
|
|
allowNull: true
|
|
},
|
|
onDelete: 'CASCADE'
|
|
})
|
|
Video: Awaited<VideoModel>
|
|
|
|
@ForeignKey(() => VideoStreamingPlaylistModel)
|
|
@Column
|
|
videoStreamingPlaylistId: number
|
|
|
|
@BelongsTo(() => VideoStreamingPlaylistModel, {
|
|
foreignKey: {
|
|
allowNull: true
|
|
},
|
|
onDelete: 'CASCADE'
|
|
})
|
|
VideoStreamingPlaylist: Awaited<VideoStreamingPlaylistModel>
|
|
|
|
static doesInfohashExistCached = memoizee(VideoFileModel.doesInfohashExist.bind(VideoFileModel), {
|
|
promise: true,
|
|
max: MEMOIZE_LENGTH.INFO_HASH_EXISTS,
|
|
maxAge: MEMOIZE_TTL.INFO_HASH_EXISTS
|
|
})
|
|
|
|
static doesInfohashExist (infoHash: string) {
|
|
const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1'
|
|
|
|
return doesExist({ sequelize: this.sequelize, query, bind: { infoHash } })
|
|
}
|
|
|
|
static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) {
|
|
const videoFile = await VideoFileModel.loadWithVideoOrPlaylist(id, videoIdOrUUID)
|
|
|
|
return !!videoFile
|
|
}
|
|
|
|
static async doesOwnedTorrentFileExist (filename: string) {
|
|
const query = 'SELECT 1 FROM "videoFile" ' +
|
|
'LEFT JOIN "video" "webvideo" ON "webvideo"."id" = "videoFile"."videoId" AND "webvideo"."remote" IS FALSE ' +
|
|
'LEFT JOIN "videoStreamingPlaylist" ON "videoStreamingPlaylist"."id" = "videoFile"."videoStreamingPlaylistId" ' +
|
|
'LEFT JOIN "video" "hlsVideo" ON "hlsVideo"."id" = "videoStreamingPlaylist"."videoId" AND "hlsVideo"."remote" IS FALSE ' +
|
|
'WHERE "torrentFilename" = $filename AND ("hlsVideo"."id" IS NOT NULL OR "webvideo"."id" IS NOT NULL) LIMIT 1'
|
|
|
|
return doesExist({ sequelize: this.sequelize, query, bind: { filename } })
|
|
}
|
|
|
|
static async doesOwnedWebVideoFileExist (filename: string, storage: FileStorageType) {
|
|
const query = 'SELECT 1 FROM "videoFile" INNER JOIN "video" ON "video"."id" = "videoFile"."videoId" AND "video"."remote" IS FALSE ' +
|
|
`WHERE "filename" = $filename AND "storage" = $storage LIMIT 1`
|
|
|
|
return doesExist({ sequelize: this.sequelize, query, bind: { filename, storage } })
|
|
}
|
|
|
|
static loadByFilename (filename: string) {
|
|
const query = {
|
|
where: {
|
|
filename
|
|
}
|
|
}
|
|
|
|
return VideoFileModel.findOne(query)
|
|
}
|
|
|
|
static loadWithVideoByFilename (filename: string): Promise<MVideoFileVideo | MVideoFileStreamingPlaylistVideo> {
|
|
const query = {
|
|
where: {
|
|
filename
|
|
}
|
|
}
|
|
|
|
return VideoFileModel.scope(ScopeNames.WITH_VIDEO_OR_PLAYLIST).findOne(query)
|
|
}
|
|
|
|
static loadWithVideoOrPlaylistByTorrentFilename (filename: string) {
|
|
const query = {
|
|
where: {
|
|
torrentFilename: filename
|
|
}
|
|
}
|
|
|
|
return VideoFileModel.scope(ScopeNames.WITH_VIDEO_OR_PLAYLIST).findOne(query)
|
|
}
|
|
|
|
static load (id: number): Promise<MVideoFile> {
|
|
return VideoFileModel.findByPk(id)
|
|
}
|
|
|
|
static loadWithMetadata (id: number) {
|
|
return VideoFileModel.scope(ScopeNames.WITH_METADATA).findByPk<MVideoFile>(id)
|
|
}
|
|
|
|
static loadWithVideo (id: number, transaction?: Transaction) {
|
|
return VideoFileModel.scope(ScopeNames.WITH_VIDEO).findByPk<MVideoFileVideo>(id, { transaction })
|
|
}
|
|
|
|
static loadWithVideoOrPlaylist (id: number, videoIdOrUUID: number | string) {
|
|
const whereVideo = validator.default.isUUID(videoIdOrUUID + '')
|
|
? { uuid: videoIdOrUUID }
|
|
: { id: videoIdOrUUID }
|
|
|
|
const options = {
|
|
where: {
|
|
id
|
|
}
|
|
}
|
|
|
|
return VideoFileModel.scope({ method: [ ScopeNames.WITH_VIDEO_OR_PLAYLIST, { whereVideo } ] })
|
|
.findOne(options)
|
|
.then(file => {
|
|
if (!file) return null
|
|
|
|
// We used `required: false` so check we have at least a video or a streaming playlist
|
|
if (!file.Video && !file.VideoStreamingPlaylist) return null
|
|
|
|
return file
|
|
})
|
|
}
|
|
|
|
static listByStreamingPlaylist (streamingPlaylistId: number, transaction: Transaction) {
|
|
const query = {
|
|
include: [
|
|
{
|
|
model: VideoStreamingPlaylistModel.unscoped(),
|
|
required: true,
|
|
where: {
|
|
id: streamingPlaylistId
|
|
}
|
|
}
|
|
],
|
|
transaction
|
|
}
|
|
|
|
return VideoFileModel.findAll<MVideoFile>(query)
|
|
}
|
|
|
|
static getStats () {
|
|
const webVideoFilesQuery: FindOptions = {
|
|
include: [
|
|
{
|
|
attributes: [],
|
|
required: true,
|
|
model: VideoModel.unscoped(),
|
|
where: {
|
|
remote: false
|
|
}
|
|
}
|
|
]
|
|
}
|
|
|
|
const hlsFilesQuery: FindOptions = {
|
|
include: [
|
|
{
|
|
attributes: [],
|
|
required: true,
|
|
model: VideoStreamingPlaylistModel.unscoped(),
|
|
include: [
|
|
{
|
|
attributes: [],
|
|
model: VideoModel.unscoped(),
|
|
required: true,
|
|
where: {
|
|
remote: false
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
|
|
return Promise.all([
|
|
VideoFileModel.aggregate('size', 'SUM', webVideoFilesQuery),
|
|
VideoFileModel.aggregate('size', 'SUM', hlsFilesQuery)
|
|
]).then(([ webVideoResult, hlsResult ]) => ({
|
|
totalLocalVideoFilesSize: parseAggregateResult(webVideoResult) + parseAggregateResult(hlsResult)
|
|
}))
|
|
}
|
|
|
|
// Redefine upsert because sequelize does not use an appropriate where clause in the update query with 2 unique indexes
|
|
static async customUpsert (
|
|
videoFile: MVideoFile,
|
|
mode: 'streaming-playlist' | 'video',
|
|
transaction: Transaction
|
|
) {
|
|
const baseFind = {
|
|
fps: videoFile.fps,
|
|
resolution: videoFile.resolution,
|
|
transaction
|
|
}
|
|
|
|
const element = mode === 'streaming-playlist'
|
|
? await VideoFileModel.loadHLSFile({ ...baseFind, playlistId: videoFile.videoStreamingPlaylistId })
|
|
: await VideoFileModel.loadWebVideoFile({ ...baseFind, videoId: videoFile.videoId })
|
|
|
|
if (!element) return videoFile.save({ transaction })
|
|
|
|
for (const k of Object.keys(videoFile.toJSON())) {
|
|
element.set(k, videoFile[k])
|
|
}
|
|
|
|
return element.save({ transaction })
|
|
}
|
|
|
|
static async loadWebVideoFile (options: {
|
|
videoId: number
|
|
fps: number
|
|
resolution: number
|
|
transaction?: Transaction
|
|
}) {
|
|
const where = {
|
|
fps: options.fps,
|
|
resolution: options.resolution,
|
|
videoId: options.videoId
|
|
}
|
|
|
|
return VideoFileModel.findOne({ where, transaction: options.transaction })
|
|
}
|
|
|
|
static async loadHLSFile (options: {
|
|
playlistId: number
|
|
fps: number
|
|
resolution: number
|
|
transaction?: Transaction
|
|
}) {
|
|
const where = {
|
|
fps: options.fps,
|
|
resolution: options.resolution,
|
|
videoStreamingPlaylistId: options.playlistId
|
|
}
|
|
|
|
return VideoFileModel.findOne({ where, transaction: options.transaction })
|
|
}
|
|
|
|
static removeHLSFilesOfStreamingPlaylistId (videoStreamingPlaylistId: number) {
|
|
const options = {
|
|
where: { videoStreamingPlaylistId }
|
|
}
|
|
|
|
return VideoFileModel.destroy(options)
|
|
}
|
|
|
|
hasTorrent () {
|
|
return this.infoHash && this.torrentFilename
|
|
}
|
|
|
|
getVideoOrStreamingPlaylist (this: MVideoFileVideo | MVideoFileStreamingPlaylistVideo): MVideo | MStreamingPlaylistVideo {
|
|
if (this.videoId || (this as MVideoFileVideo).Video) return (this as MVideoFileVideo).Video
|
|
|
|
return (this as MVideoFileStreamingPlaylistVideo).VideoStreamingPlaylist
|
|
}
|
|
|
|
getVideo (this: MVideoFileVideo | MVideoFileStreamingPlaylistVideo): MVideo {
|
|
return extractVideo(this.getVideoOrStreamingPlaylist())
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
isAudio () {
|
|
return this.resolution === VideoResolution.H_NOVIDEO
|
|
}
|
|
|
|
isLive () {
|
|
return this.size === -1
|
|
}
|
|
|
|
isHLS () {
|
|
return !!this.videoStreamingPlaylistId
|
|
}
|
|
|
|
hasAudio () {
|
|
return (this.streams & VideoFileStream.AUDIO) === VideoFileStream.AUDIO
|
|
}
|
|
|
|
hasVideo () {
|
|
return (this.streams & VideoFileStream.VIDEO) === VideoFileStream.VIDEO
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
getObjectStorageUrl (video: MVideo) {
|
|
if (video.hasPrivateStaticPath() && CONFIG.OBJECT_STORAGE.PROXY.PROXIFY_PRIVATE_FILES === true) {
|
|
return this.getPrivateObjectStorageUrl(video)
|
|
}
|
|
|
|
return this.getPublicObjectStorageUrl()
|
|
}
|
|
|
|
private getPrivateObjectStorageUrl (video: MVideo) {
|
|
if (this.isHLS()) {
|
|
return getHLSPrivateFileUrl(video, this.filename)
|
|
}
|
|
|
|
return getWebVideoPrivateFileUrl(this.filename)
|
|
}
|
|
|
|
private getPublicObjectStorageUrl () {
|
|
if (this.isHLS()) {
|
|
return getObjectStoragePublicFileUrl(this.fileUrl, CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS)
|
|
}
|
|
|
|
return getObjectStoragePublicFileUrl(this.fileUrl, CONFIG.OBJECT_STORAGE.WEB_VIDEOS)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
getFileUrl (video: MVideo) {
|
|
if (video.isOwned()) {
|
|
if (this.storage === FileStorage.OBJECT_STORAGE) {
|
|
return this.getObjectStorageUrl(video)
|
|
}
|
|
|
|
return WEBSERVER.URL + this.getFileStaticPath(video)
|
|
}
|
|
|
|
return this.fileUrl
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
getFileStaticPath (video: MVideo) {
|
|
if (this.isHLS()) return this.getHLSFileStaticPath(video)
|
|
|
|
return this.getWebVideoFileStaticPath(video)
|
|
}
|
|
|
|
private getWebVideoFileStaticPath (video: MVideo) {
|
|
if (isVideoInPrivateDirectory(video.privacy)) {
|
|
return join(STATIC_PATHS.PRIVATE_WEB_VIDEOS, this.filename)
|
|
}
|
|
|
|
return join(STATIC_PATHS.WEB_VIDEOS, this.filename)
|
|
}
|
|
|
|
private getHLSFileStaticPath (video: MVideo) {
|
|
if (isVideoInPrivateDirectory(video.privacy)) {
|
|
return join(STATIC_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS, video.uuid, this.filename)
|
|
}
|
|
|
|
return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, video.uuid, this.filename)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
getFileDownloadUrl (video: MVideoWithHost) {
|
|
const path = this.isHLS()
|
|
? join(DOWNLOAD_PATHS.HLS_VIDEOS, `${video.uuid}-${this.resolution}-fragmented${this.extname}`)
|
|
: join(DOWNLOAD_PATHS.WEB_VIDEOS, `${video.uuid}-${this.resolution}${this.extname}`)
|
|
|
|
if (video.isOwned()) return WEBSERVER.URL + path
|
|
|
|
// FIXME: don't guess remote URL
|
|
return buildRemoteUrl(video, path)
|
|
}
|
|
|
|
getRemoteTorrentUrl (video: MVideo) {
|
|
if (video.isOwned()) throw new Error(`Video ${video.url} is not a remote video`)
|
|
|
|
return this.torrentUrl
|
|
}
|
|
|
|
// We proxify torrent requests so use a local URL
|
|
getTorrentUrl () {
|
|
if (!this.torrentFilename) return null
|
|
|
|
return WEBSERVER.URL + this.getTorrentStaticPath()
|
|
}
|
|
|
|
getTorrentStaticPath () {
|
|
if (!this.torrentFilename) return null
|
|
|
|
return join(LAZY_STATIC_PATHS.TORRENTS, this.torrentFilename)
|
|
}
|
|
|
|
getTorrentDownloadUrl () {
|
|
if (!this.torrentFilename) return null
|
|
|
|
return WEBSERVER.URL + join(DOWNLOAD_PATHS.TORRENTS, this.torrentFilename)
|
|
}
|
|
|
|
removeTorrent () {
|
|
if (!this.torrentFilename) return null
|
|
|
|
const torrentPath = getFSTorrentFilePath(this)
|
|
return remove(torrentPath)
|
|
.catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
|
|
}
|
|
|
|
hasSameUniqueKeysThan (other: MVideoFile) {
|
|
return this.fps === other.fps &&
|
|
this.resolution === other.resolution &&
|
|
(
|
|
(this.videoId !== null && this.videoId === other.videoId) ||
|
|
(this.videoStreamingPlaylistId !== null && this.videoStreamingPlaylistId === other.videoStreamingPlaylistId)
|
|
)
|
|
}
|
|
|
|
withVideoOrPlaylist (videoOrPlaylist: MVideo | MStreamingPlaylistVideo) {
|
|
if (isStreamingPlaylist(videoOrPlaylist)) return Object.assign(this, { VideoStreamingPlaylist: videoOrPlaylist })
|
|
|
|
return Object.assign(this, { Video: videoOrPlaylist })
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
toActivityPubObject (this: MVideoFile, video: MVideo): ActivityVideoUrlObject {
|
|
const mimeType = getVideoFileMimeType(this.extname, false)
|
|
|
|
const attachment: ActivityVideoUrlObject['attachment'] = []
|
|
|
|
if (this.hasAudio()) {
|
|
attachment.push({
|
|
type: 'PropertyValue',
|
|
name: 'ffprobe_codec_type',
|
|
value: 'audio'
|
|
})
|
|
}
|
|
|
|
if (this.hasVideo()) {
|
|
attachment.push({
|
|
type: 'PropertyValue',
|
|
name: 'ffprobe_codec_type',
|
|
value: 'video'
|
|
})
|
|
}
|
|
|
|
if (this.formatFlags & VideoFileFormatFlag.FRAGMENTED) {
|
|
attachment.push({
|
|
type: 'PropertyValue',
|
|
name: 'peertube_format_flag',
|
|
value: 'fragmented'
|
|
})
|
|
}
|
|
|
|
if (this.formatFlags & VideoFileFormatFlag.WEB_VIDEO) {
|
|
attachment.push({
|
|
type: 'PropertyValue',
|
|
name: 'peertube_format_flag',
|
|
value: 'web-video'
|
|
})
|
|
}
|
|
|
|
return {
|
|
type: 'Link',
|
|
mediaType: mimeType as ActivityVideoUrlObject['mediaType'],
|
|
href: this.getFileUrl(video),
|
|
height: this.height || this.resolution,
|
|
width: this.width,
|
|
size: this.size,
|
|
fps: this.fps,
|
|
attachment
|
|
}
|
|
}
|
|
}
|