// @ts-ignore
import * as videojs from 'video.js'
-import { VideoFile } from '../../../../shared/models/videos/video.model'
import { PeerTubePlugin } from './peertube-plugin'
import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin'
import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin'
import { PlayerMode } from './peertube-player-manager'
import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
+import { VideoFile } from '@shared/models'
declare namespace videojs {
interface Player {
import * as videojs from 'video.js'
import * as WebTorrent from 'webtorrent'
-import { VideoFile } from '../../../../../shared/models/videos/video.model'
import { renderVideo } from './video-renderer'
import { LoadedQualityData, PlayerNetworkInfo, VideoJSComponentInterface, WebtorrentPluginOptions } from '../peertube-videojs-typings'
import { getRtcConfig, timeToInt, videoFileMaxByResolution, videoFileMinByResolution } from '../utils'
getStoredWebTorrentEnabled,
saveAverageBandwidth
} from '../peertube-player-local-storage'
+import { VideoFile } from '@shared/models'
const CacheChunkStore = require('cache-chunk-store')
720p: false
1080p: false
2160p: false
+
+ # Generate videos in a WebTorrent format (what we do since the first PeerTube release)
+ # If you also enabled the hls format, it will multiply videos storage by 2
+ webtorrent:
+ enabled: true
+
# /!\ Requires ffmpeg >= 4.1
# Generate HLS playlists and fragmented MP4 files. Better playback than with WebTorrent:
# * Resolution change is smoother
# * Faster playback in particular with long videos
# * More stable playback (less bugs/infinite loading)
- # /!\ Multiplies videos storage by 2 /!\
+ # If you also enabled the webtorrent format, it will multiply videos storage by 2
hls:
enabled: false
720p: false
1080p: false
2160p: false
+
+ # Generate videos in a WebTorrent format (what we do since the first PeerTube release)
+ # If you also enabled the hls format, it will multiply videos storage by 2
+ webtorrent:
+ enabled: true
+
# /!\ Requires ffmpeg >= 4.1
# Generate HLS playlists and fragmented MP4 files. Better playback than with WebTorrent:
# * Resolution change is smoother
# * Faster playback in particular with long videos
# * More stable playback (less bugs/infinite loading)
- # /!\ Multiplies videos storage by 2 /!\
+ # If you also enabled the webtorrent format, it will multiply videos storage by 2
hls:
enabled: false
"ts-node": "8.4.1",
"tslint": "^5.7.0",
"tslint-config-standard": "^8.0.1",
- "typescript": "^3.4.3",
+ "typescript": "^3.7.2",
"xliff": "^4.0.0"
},
"scripty": {
import { registerTSPaths } from '../server/helpers/register-ts-paths'
-registerTSPaths()
-
import { VIDEO_TRANSCODING_FPS } from '../server/initializers/constants'
import { getDurationFromVideoFile, getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../server/helpers/ffmpeg-utils'
import { getMaxBitrate } from '../shared/models/videos'
import { VideoModel } from '../server/models/video/video'
-import { optimizeVideofile } from '../server/lib/video-transcoding'
+import { optimizeOriginalVideofile } from '../server/lib/video-transcoding'
import { initDatabaseModels } from '../server/initializers'
-import { basename, dirname, join } from 'path'
+import { basename, dirname } from 'path'
import { copy, move, remove } from 'fs-extra'
-import { CONFIG } from '../server/initializers/config'
+import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
+import { getVideoFilePath } from '@server/lib/video-paths'
+
+registerTSPaths()
run()
.then(() => process.exit(0))
currentVideoId = video.id
for (const file of video.VideoFiles) {
- currentFile = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(file))
+ currentFile = getVideoFilePath(video, file)
const [ videoBitrate, fps, resolution ] = await Promise.all([
getVideoFileBitrate(currentFile),
const backupFile = `${currentFile}_backup`
await copy(currentFile, backupFile)
- await optimizeVideofile(video, file)
+ await optimizeOriginalVideofile(video, file)
const originalDuration = await getDurationFromVideoFile(backupFile)
const newDuration = await getDurationFromVideoFile(currentFile)
console.log('Failed to optimize %s, restoring original', basename(currentFile))
await move(backupFile, currentFile, { overwrite: true })
- await video.createTorrentAndSetInfoHash(file)
+ await createTorrentAndSetInfoHash(video, file)
await file.save()
}
}
return true
}
- const videoFile = video.getFile(resolution)
+ const videoFile = video.getWebTorrentFile(resolution)
if (!videoFile) {
- console.error('Cannot find file of video %s - %d', video.url, resolution)
+ console.error('Cannot find webtorrent file of video %s - %d', video.url, resolution)
return true
}
import { registerTSPaths } from '../server/helpers/register-ts-paths'
-registerTSPaths()
-
import { WEBSERVER } from '../server/initializers/constants'
import { ActorFollowModel } from '../server/models/activitypub/actor-follow'
import { VideoModel } from '../server/models/video/video'
import { VideoChannelModel } from '../server/models/video/video-channel'
import { VideoStreamingPlaylistModel } from '../server/models/video/video-streaming-playlist'
import { initDatabaseModels } from '../server/initializers'
+import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
+
+registerTSPaths()
run()
.then(() => process.exit(0))
for (const file of video.VideoFiles) {
console.log('Updating torrent file %s of video %s.', file.resolution, video.uuid)
- await video.createTorrentAndSetInfoHash(file)
+ await createTorrentAndSetInfoHash(video, file)
}
for (const playlist of video.VideoStreamingPlaylists) {
hls: {
enabled: CONFIG.TRANSCODING.HLS.ENABLED
},
+ webtorrent: {
+ enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED
+ },
enabledResolutions: getEnabledResolutions()
},
import: {
'1080p': CONFIG.TRANSCODING.RESOLUTIONS[ '1080p' ],
'2160p': CONFIG.TRANSCODING.RESOLUTIONS[ '2160p' ]
},
+ webtorrent: {
+ enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED
+ },
hls: {
enabled: CONFIG.TRANSCODING.HLS.ENABLED
}
import { VideoTranscodingPayload } from '../../../lib/job-queue/handlers/video-transcoding'
import { Hooks } from '../../../lib/plugins/hooks'
import { MVideoDetails, MVideoFullLight } from '@server/typings/models'
+import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
+import { getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
const auditLogger = auditLoggerFactory('videos')
const videosRouter = express.Router()
const videoFile = new VideoFileModel({
extname: extname(videoPhysicalFile.filename),
- size: videoPhysicalFile.size
+ size: videoPhysicalFile.size,
+ videoStreamingPlaylistId: null
})
if (videoFile.isAudio()) {
}
// Move physical file
- const videoDir = CONFIG.STORAGE.VIDEOS_DIR
- const destination = join(videoDir, video.getVideoFilename(videoFile))
+ const destination = getVideoFilePath(video, videoFile)
await move(videoPhysicalFile.path, destination)
// This is important in case if there is another attempt in the retry process
- videoPhysicalFile.filename = video.getVideoFilename(videoFile)
+ videoPhysicalFile.filename = getVideoFilePath(video, videoFile)
videoPhysicalFile.path = destination
// Process thumbnail or create it from the video
: await generateVideoMiniature(video, videoFile, ThumbnailType.PREVIEW)
// Create the torrent file
- await video.createTorrentAndSetInfoHash(videoFile)
+ await createTorrentAndSetInfoHash(video, videoFile)
const { videoCreated } = await sequelizeTypescript.transaction(async t => {
const sequelizeOptions = { transaction: t }
import { root } from '../helpers/core-utils'
import { CONFIG } from '../initializers/config'
import { getPreview, getVideoCaption } from './lazy-static'
+import { VideoStreamingPlaylistType } from '@shared/models/videos/video-streaming-playlist.type'
+import { MVideoFile, MVideoFullLight } from '@server/typings/models'
+import { getTorrentFilePath, getVideoFilePath } from '@server/lib/video-paths'
const staticRouter = express.Router()
asyncMiddleware(videosGetValidator),
asyncMiddleware(downloadTorrent)
)
+staticRouter.use(
+ STATIC_DOWNLOAD_PATHS.TORRENTS + ':id-:resolution([0-9]+)-hls.torrent',
+ asyncMiddleware(videosGetValidator),
+ asyncMiddleware(downloadHLSVideoFileTorrent)
+)
// Videos path for webseeding
staticRouter.use(
asyncMiddleware(downloadVideoFile)
)
+staticRouter.use(
+ STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+).:extension',
+ asyncMiddleware(videosGetValidator),
+ asyncMiddleware(downloadHLSVideoFile)
+)
+
// HLS
staticRouter.use(
STATIC_PATHS.STREAMING_PLAYLISTS.HLS,
}
async function downloadTorrent (req: express.Request, res: express.Response) {
- const { video, videoFile } = getVideoAndFile(req, res)
+ const video = res.locals.videoAll
+
+ const videoFile = getVideoFile(req, video.VideoFiles)
+ if (!videoFile) return res.status(404).end()
+
+ return res.download(getTorrentFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p.torrent`)
+}
+
+async function downloadHLSVideoFileTorrent (req: express.Request, res: express.Response) {
+ const video = res.locals.videoAll
+
+ const playlist = getHLSPlaylist(video)
+ if (!playlist) return res.status(404).end
+
+ const videoFile = getVideoFile(req, playlist.VideoFiles)
if (!videoFile) return res.status(404).end()
- return res.download(video.getTorrentFilePath(videoFile), `${video.name}-${videoFile.resolution}p.torrent`)
+ return res.download(getTorrentFilePath(playlist, videoFile), `${video.name}-${videoFile.resolution}p-hls.torrent`)
}
async function downloadVideoFile (req: express.Request, res: express.Response) {
- const { video, videoFile } = getVideoAndFile(req, res)
+ const video = res.locals.videoAll
+
+ const videoFile = getVideoFile(req, video.VideoFiles)
if (!videoFile) return res.status(404).end()
- return res.download(video.getVideoFilePath(videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`)
+ return res.download(getVideoFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`)
}
-function getVideoAndFile (req: express.Request, res: express.Response) {
- const resolution = parseInt(req.params.resolution, 10)
+async function downloadHLSVideoFile (req: express.Request, res: express.Response) {
const video = res.locals.videoAll
+ const playlist = getHLSPlaylist(video)
+ if (!playlist) return res.status(404).end
+
+ const videoFile = getVideoFile(req, playlist.VideoFiles)
+ if (!videoFile) return res.status(404).end()
+
+ const filename = `${video.name}-${videoFile.resolution}p-${playlist.getStringType()}${videoFile.extname}`
+ return res.download(getVideoFilePath(playlist, videoFile), filename)
+}
+
+function getVideoFile (req: express.Request, files: MVideoFile[]) {
+ const resolution = parseInt(req.params.resolution, 10)
+ return files.find(f => f.resolution === resolution)
+}
- const videoFile = video.VideoFiles.find(f => f.resolution === resolution)
+function getHLSPlaylist (video: MVideoFullLight) {
+ const playlist = video.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
+ if (!playlist) return undefined
- return { video, videoFile }
+ return Object.assign(playlist, { Video: video })
}
} from '../videos'
import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
import { VideoState } from '../../../../shared/models/videos'
+import { logger } from '@server/helpers/logger'
function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) {
return isBaseActivityValid(activity, 'Update') &&
function sanitizeAndCheckVideoTorrentObject (video: any) {
if (!video || video.type !== 'Video') return false
- if (!setValidRemoteTags(video)) return false
- if (!setValidRemoteVideoUrls(video)) return false
- if (!setRemoteVideoTruncatedContent(video)) return false
- if (!setValidAttributedTo(video)) return false
- if (!setValidRemoteCaptions(video)) return false
+ if (!setValidRemoteTags(video)) {
+ logger.debug('Video has invalid tags', { video })
+ return false
+ }
+ if (!setValidRemoteVideoUrls(video)) {
+ logger.debug('Video has invalid urls', { video })
+ return false
+ }
+ if (!setRemoteVideoTruncatedContent(video)) {
+ logger.debug('Video has invalid content', { video })
+ return false
+ }
+ if (!setValidAttributedTo(video)) {
+ logger.debug('Video has invalid attributedTo', { video })
+ return false
+ }
+ if (!setValidRemoteCaptions(video)) {
+ logger.debug('Video has invalid captions', { video })
+ return false
+ }
// Default attributes
if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED
}
function isRemoteVideoUrlValid (url: any) {
- // FIXME: Old bug, we used the width to represent the resolution. Remove it in a few release (currently beta.11)
- if (url.width && !url.height) url.height = url.width
-
return url.type === 'Link' &&
(
- // TODO: remove mimeType (backward compatibility, introduced in v1.1.0)
- ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.indexOf(url.mediaType || url.mimeType) !== -1 &&
+ ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.indexOf(url.mediaType) !== -1 &&
isActivityPubUrlValid(url.href) &&
validator.isInt(url.height + '', { min: 0 }) &&
validator.isInt(url.size + '', { min: 0 }) &&
(!url.fps || validator.isInt(url.fps + '', { min: -1 }))
) ||
(
- ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.indexOf(url.mediaType || url.mimeType) !== -1 &&
+ ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.indexOf(url.mediaType) !== -1 &&
isActivityPubUrlValid(url.href) &&
validator.isInt(url.height + '', { min: 0 })
) ||
(
- ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mediaType || url.mimeType) !== -1 &&
+ ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mediaType) !== -1 &&
validator.isLength(url.href, { min: 5 }) &&
validator.isInt(url.height + '', { min: 0 })
) ||
return fn()
}
+function deleteNonExistingModels <T extends { hasSameUniqueKeysThan (other: T): boolean } & Model<T>> (
+ fromDatabase: T[],
+ newModels: T[],
+ t: Transaction
+) {
+ return fromDatabase.filter(f => !newModels.find(newModel => newModel.hasSameUniqueKeysThan(f)))
+ .map(f => f.destroy({ transaction: t }))
+}
+
// ---------------------------------------------------------------------------
export {
retryTransactionWrapper,
transactionRetryer,
updateInstanceWithAnother,
- afterCommitIfTransaction
+ afterCommitIfTransaction,
+ deleteNonExistingModels
}
interface HLSTranscodeOptions extends BaseTranscodeOptions {
type: 'hls'
+ copyCodecs: boolean
hlsPlaylist: {
videoFilename: string
}
// ---------------------------------------------------------------------------
-async function buildx264Command (command: ffmpeg.FfmpegCommand, options: VideoTranscodeOptions) {
+async function buildx264Command (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) {
let fps = await getVideoFileFPS(options.inputPath)
// On small/medium resolutions, limit FPS
if (
async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: HLSTranscodeOptions) {
const videoPath = getHLSVideoPath(options)
- command = await presetCopy(command)
+ if (options.copyCodecs) command = await presetCopy(command)
+ else command = await buildx264Command(command, options)
command = command.outputOption('-hls_time 4')
.outputOption('-hls_list_size 0')
if (fetchType === 'only-video') return VideoModel.loadByUrl(url)
}
-function getVideo (res: Response) {
- return res.locals.videoAll || res.locals.onlyVideo || res.locals.onlyVideoWithRights || res.locals.videoId
-}
-
function getVideoWithAttributes (res: Response) {
return res.locals.videoAll || res.locals.onlyVideo || res.locals.onlyVideoWithRights
}
VideoFetchType,
VideoFetchByUrlType,
fetchVideo,
- getVideo,
getVideoWithAttributes,
fetchVideoByUrl
}
import { logger } from './logger'
import { generateVideoImportTmpPath } from './utils'
import * as WebTorrent from 'webtorrent'
-import { createWriteStream, ensureDir, remove } from 'fs-extra'
+import { createWriteStream, ensureDir, remove, writeFile } from 'fs-extra'
import { CONFIG } from '../initializers/config'
import { dirname, join } from 'path'
import * as createTorrent from 'create-torrent'
import { promisify2 } from './core-utils'
+import { MVideo } from '@server/typings/models/video/video'
+import { MVideoFile, MVideoFileRedundanciesOpt } from '@server/typings/models/video/video-file'
+import { isStreamingPlaylist, MStreamingPlaylistVideo } from '@server/typings/models/video/video-streaming-playlist'
+import { STATIC_PATHS, WEBSERVER } from '@server/initializers/constants'
+import * as parseTorrent from 'parse-torrent'
+import * as magnetUtil from 'magnet-uri'
+import { isArray } from '@server/helpers/custom-validators/misc'
+import { extractVideo } from '@server/lib/videos'
+import { getTorrentFileName, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
+
+const createTorrentPromise = promisify2<string, any, any>(createTorrent)
async function downloadWebTorrentVideo (target: { magnetUri: string, torrentName?: string }, timeout: number) {
const id = target.magnetUri || target.torrentName
})
}
-const createTorrentPromise = promisify2<string, any, any>(createTorrent)
+async function createTorrentAndSetInfoHash (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile) {
+ const video = extractVideo(videoOrPlaylist)
+
+ const options = {
+ // Keep the extname, it's used by the client to stream the file inside a web browser
+ name: `${video.name} ${videoFile.resolution}p${videoFile.extname}`,
+ createdBy: 'PeerTube',
+ announceList: [
+ [ WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT + '/tracker/socket' ],
+ [ WEBSERVER.URL + '/tracker/announce' ]
+ ],
+ urlList: [ WEBSERVER.URL + STATIC_PATHS.WEBSEED + getVideoFilename(videoOrPlaylist, videoFile) ]
+ }
+
+ const torrent = await createTorrentPromise(getVideoFilePath(videoOrPlaylist, videoFile), options)
+
+ const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, getTorrentFileName(videoOrPlaylist, videoFile))
+ logger.info('Creating torrent %s.', filePath)
+
+ await writeFile(filePath, torrent)
+
+ const parsedTorrent = parseTorrent(torrent)
+ videoFile.infoHash = parsedTorrent.infoHash
+}
+
+function generateMagnetUri (
+ videoOrPlaylist: MVideo | MStreamingPlaylistVideo,
+ videoFile: MVideoFileRedundanciesOpt,
+ baseUrlHttp: string,
+ baseUrlWs: string
+) {
+ const video = isStreamingPlaylist(videoOrPlaylist)
+ ? videoOrPlaylist.Video
+ : videoOrPlaylist
+
+ const xs = videoOrPlaylist.getTorrentUrl(videoFile, baseUrlHttp)
+ const announce = videoOrPlaylist.getTrackerUrls(baseUrlHttp, baseUrlWs)
+ let urlList = [ videoOrPlaylist.getVideoFileUrl(videoFile, baseUrlHttp) ]
+
+ const redundancies = videoFile.RedundancyVideos
+ if (isArray(redundancies)) urlList = urlList.concat(redundancies.map(r => r.fileUrl))
+
+ const magnetHash = {
+ xs,
+ announce,
+ urlList,
+ infoHash: videoFile.infoHash,
+ name: video.name
+ }
+
+ return magnetUtil.encode(magnetHash)
+}
// ---------------------------------------------------------------------------
export {
- createTorrentPromise,
+ createTorrentAndSetInfoHash,
+ generateMagnetUri,
downloadWebTorrentVideo
}
}
}
+ // Transcoding
+ if (CONFIG.TRANSCODING.ENABLED) {
+ if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false && CONFIG.TRANSCODING.HLS.ENABLED === false) {
+ return 'You need to enable at least WebTorrent transcoding or HLS transcoding.'
+ }
+ }
+
return null
}
},
HLS: {
get ENABLED () { return config.get<boolean>('transcoding.hls.enabled') }
+ },
+ WEBTORRENT: {
+ get ENABLED () { return config.get<boolean>('transcoding.webtorrent.enabled') }
}
},
IMPORT: {
// ---------------------------------------------------------------------------
-const LAST_MIGRATION_VERSION = 445
+const LAST_MIGRATION_VERSION = 450
// ---------------------------------------------------------------------------
}
const STATIC_DOWNLOAD_PATHS = {
TORRENTS: '/download/torrents/',
- VIDEOS: '/download/videos/'
+ VIDEOS: '/download/videos/',
+ HLS_VIDEOS: '/download/streaming-playlists/hls/videos/'
}
const LAZY_STATIC_PATHS = {
AVATARS: '/lazy-static/avatars/',
import * as Promise from 'bluebird'
import { stat } from 'fs-extra'
import { VideoModel } from '../../models/video/video'
+import { getVideoFilePath } from '@server/lib/video-paths'
function up (utils: {
transaction: Sequelize.Transaction,
videos.forEach(video => {
video.VideoFiles.forEach(videoFile => {
const p = new Promise((res, rej) => {
- stat(video.getVideoFilePath(videoFile), (err, stats) => {
+ stat(getVideoFilePath(video, videoFile), (err, stats) => {
if (err) return rej(err)
videoFile.size = stats.size
--- /dev/null
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+ transaction: Sequelize.Transaction,
+ queryInterface: Sequelize.QueryInterface,
+ sequelize: Sequelize.Sequelize,
+ db: any
+}): Promise<void> {
+ {
+ const data = {
+ type: Sequelize.INTEGER,
+ allowNull: true,
+ references: {
+ model: 'videoStreamingPlaylist',
+ key: 'id'
+ },
+ onDelete: 'CASCADE'
+ }
+
+ await utils.queryInterface.addColumn('videoFile', 'videoStreamingPlaylistId', data)
+ }
+
+ {
+ const data = {
+ type: Sequelize.INTEGER,
+ allowNull: true
+ }
+
+ await utils.queryInterface.changeColumn('videoFile', 'videoId', data)
+ }
+}
+
+function down (options) {
+ throw new Error('Not implemented.')
+}
+
+export {
+ up,
+ down
+}
import * as magnetUtil from 'magnet-uri'
import * as request from 'request'
import {
+ ActivityHashTagObject,
+ ActivityMagnetUrlObject,
ActivityPlaylistSegmentHashesObject,
- ActivityPlaylistUrlObject,
+ ActivityPlaylistUrlObject, ActivityTagObject,
ActivityUrlObject,
ActivityVideoUrlObject,
VideoState
import { VideoPrivacy } from '../../../shared/models/videos'
import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos'
import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
-import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
+import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
import { logger } from '../../helpers/logger'
import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
import {
MChannelAccountLight,
MChannelDefault,
MChannelId,
+ MStreamingPlaylist,
MVideo,
MVideoAccountLight,
MVideoAccountLightBlacklistAllFiles,
await videoUpdated.addAndSaveThumbnail(previewModel, t)
{
- const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoUpdated, videoObject)
+ const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoUpdated, videoObject.url)
const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a))
// Remove video files that do not exist anymore
- const destroyTasks = videoUpdated.VideoFiles
- .filter(f => !newVideoFiles.find(newFile => newFile.hasSameUniqueKeysThan(f)))
- .map(f => f.destroy(sequelizeOptions))
+ const destroyTasks = deleteNonExistingModels(videoUpdated.VideoFiles, newVideoFiles, t)
await Promise.all(destroyTasks)
// Update or add other one
- const upsertTasks = videoFileAttributes.map(a => {
- return VideoFileModel.upsert<VideoFileModel>(a, { returning: true, transaction: t })
- .then(([ file ]) => file)
- })
-
+ const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'video', t))
videoUpdated.VideoFiles = await Promise.all(upsertTasks)
}
const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes(videoUpdated, videoObject, videoUpdated.VideoFiles)
const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a))
- // Remove video files that do not exist anymore
- const destroyTasks = videoUpdated.VideoStreamingPlaylists
- .filter(f => !newStreamingPlaylists.find(newPlaylist => newPlaylist.hasSameUniqueKeysThan(f)))
- .map(f => f.destroy(sequelizeOptions))
+ // Remove video playlists that do not exist anymore
+ const destroyTasks = deleteNonExistingModels(videoUpdated.VideoStreamingPlaylists, newStreamingPlaylists, t)
await Promise.all(destroyTasks)
- // Update or add other one
- const upsertTasks = streamingPlaylistAttributes.map(a => {
- return VideoStreamingPlaylistModel.upsert<VideoStreamingPlaylistModel>(a, { returning: true, transaction: t })
- .then(([ streamingPlaylist ]) => streamingPlaylist)
- })
+ let oldStreamingPlaylistFiles: MVideoFile[] = []
+ for (const videoStreamingPlaylist of videoUpdated.VideoStreamingPlaylists) {
+ oldStreamingPlaylistFiles = oldStreamingPlaylistFiles.concat(videoStreamingPlaylist.VideoFiles)
+ }
+
+ videoUpdated.VideoStreamingPlaylists = []
+
+ for (const playlistAttributes of streamingPlaylistAttributes) {
+ const streamingPlaylistModel = await VideoStreamingPlaylistModel.upsert(playlistAttributes, { returning: true, transaction: t })
+ .then(([ streamingPlaylist ]) => streamingPlaylist)
- videoUpdated.VideoStreamingPlaylists = await Promise.all(upsertTasks)
+ const newVideoFiles: MVideoFile[] = videoFileActivityUrlToDBAttributes(streamingPlaylistModel, playlistAttributes.tagAPObject)
+ .map(a => new VideoFileModel(a))
+ const destroyTasks = deleteNonExistingModels(oldStreamingPlaylistFiles, newVideoFiles, t)
+ await Promise.all(destroyTasks)
+
+ // Update or add other one
+ const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'streaming-playlist', t))
+ streamingPlaylistModel.VideoFiles = await Promise.all(upsertTasks)
+
+ videoUpdated.VideoStreamingPlaylists.push(streamingPlaylistModel)
+ }
}
{
// Update Tags
- const tags = videoObject.tag.map(tag => tag.name)
+ const tags = videoObject.tag
+ .filter(isAPHashTagObject)
+ .map(tag => tag.name)
const tagInstances = await TagModel.findOrCreateTags(tags, t)
await videoUpdated.$set('Tags', tagInstances, sequelizeOptions)
}
// ---------------------------------------------------------------------------
-function isAPVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject {
+function isAPVideoUrlObject (url: any): url is ActivityVideoUrlObject {
const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT)
- const urlMediaType = url.mediaType || url.mimeType
+ const urlMediaType = url.mediaType
return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/')
}
function isAPStreamingPlaylistUrlObject (url: ActivityUrlObject): url is ActivityPlaylistUrlObject {
- const urlMediaType = url.mediaType || url.mimeType
-
- return urlMediaType === 'application/x-mpegURL'
+ return url && url.mediaType === 'application/x-mpegURL'
}
function isAPPlaylistSegmentHashesUrlObject (tag: any): tag is ActivityPlaylistSegmentHashesObject {
- const urlMediaType = tag.mediaType || tag.mimeType
+ return tag && tag.name === 'sha256' && tag.type === 'Link' && tag.mediaType === 'application/json'
+}
+
+function isAPMagnetUrlObject (url: any): url is ActivityMagnetUrlObject {
+ return url && url.mediaType === 'application/x-bittorrent;x-scheme-handler/magnet'
+}
- return tag.name === 'sha256' && tag.type === 'Link' && urlMediaType === 'application/json'
+function isAPHashTagObject (url: any): url is ActivityHashTagObject {
+ return url && url.type === 'Hashtag'
}
async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAccountLight, waitThumbnail = false) {
if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t)
// Process files
- const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject)
- if (videoFileAttributes.length === 0) {
- throw new Error('Cannot find valid files for video %s ' + videoObject.url)
- }
+ const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject.url)
const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t }))
const videoFiles = await Promise.all(videoFilePromises)
- const videoStreamingPlaylists = streamingPlaylistActivityUrlToDBAttributes(videoCreated, videoObject, videoFiles)
- const playlistPromises = videoStreamingPlaylists.map(p => VideoStreamingPlaylistModel.create(p, { transaction: t }))
- const streamingPlaylists = await Promise.all(playlistPromises)
+ const streamingPlaylistsAttributes = streamingPlaylistActivityUrlToDBAttributes(videoCreated, videoObject, videoFiles)
+ videoCreated.VideoStreamingPlaylists = []
+
+ for (const playlistAttributes of streamingPlaylistsAttributes) {
+ const playlistModel = await VideoStreamingPlaylistModel.create(playlistAttributes, { transaction: t })
+
+ const playlistFiles = videoFileActivityUrlToDBAttributes(playlistModel, playlistAttributes.tagAPObject)
+ const videoFilePromises = playlistFiles.map(f => VideoFileModel.create(f, { transaction: t }))
+ playlistModel.VideoFiles = await Promise.all(videoFilePromises)
+
+ videoCreated.VideoStreamingPlaylists.push(playlistModel)
+ }
// Process tags
const tags = videoObject.tag
- .filter(t => t.type === 'Hashtag')
+ .filter(isAPHashTagObject)
.map(t => t.name)
const tagInstances = await TagModel.findOrCreateTags(tags, t)
await videoCreated.$set('Tags', tagInstances, sequelizeOptions)
await Promise.all(videoCaptionsPromises)
videoCreated.VideoFiles = videoFiles
- videoCreated.VideoStreamingPlaylists = streamingPlaylists
videoCreated.Tags = tagInstances
const autoBlacklisted = await autoBlacklistVideoIfNeeded({
}
}
-function videoFileActivityUrlToDBAttributes (video: MVideo, videoObject: VideoTorrentObject) {
- const fileUrls = videoObject.url.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
+function videoFileActivityUrlToDBAttributes (
+ videoOrPlaylist: MVideo | MStreamingPlaylist,
+ urls: (ActivityTagObject | ActivityUrlObject)[]
+) {
+ const fileUrls = urls.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
- if (fileUrls.length === 0) {
- throw new Error('Cannot find video files for ' + video.url)
- }
+ if (fileUrls.length === 0) return []
const attributes: FilteredModelAttributes<VideoFileModel>[] = []
for (const fileUrl of fileUrls) {
// Fetch associated magnet uri
- const magnet = videoObject.url.find(u => {
- const mediaType = u.mediaType || u.mimeType
- return mediaType === 'application/x-bittorrent;x-scheme-handler/magnet' && (u as any).height === fileUrl.height
- })
+ const magnet = urls.filter(isAPMagnetUrlObject)
+ .find(u => u.height === fileUrl.height)
if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.href)
throw new Error('Cannot parse magnet URI ' + magnet.href)
}
- const mediaType = fileUrl.mediaType || fileUrl.mimeType
+ const mediaType = fileUrl.mediaType
const attribute = {
extname: MIMETYPES.VIDEO.MIMETYPE_EXT[ mediaType ],
infoHash: parsed.infoHash,
resolution: fileUrl.height,
size: fileUrl.size,
- videoId: video.id,
- fps: fileUrl.fps || -1
+ fps: fileUrl.fps || -1,
+
+ // This is a video file owned by a video or by a streaming playlist
+ videoId: (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? null : videoOrPlaylist.id,
+ videoStreamingPlaylistId: (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? videoOrPlaylist.id : null
}
attributes.push(attribute)
const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[]
if (playlistUrls.length === 0) return []
- const attributes: FilteredModelAttributes<VideoStreamingPlaylistModel>[] = []
+ const attributes: (FilteredModelAttributes<VideoStreamingPlaylistModel> & { tagAPObject?: ActivityTagObject[] })[] = []
for (const playlistUrlObject of playlistUrls) {
- const segmentsSha256UrlObject = playlistUrlObject.tag
- .find(t => {
- return isAPPlaylistSegmentHashesUrlObject(t)
- }) as ActivityPlaylistSegmentHashesObject
+ const segmentsSha256UrlObject = playlistUrlObject.tag.find(isAPPlaylistSegmentHashesUrlObject)
+
+ let files: unknown[] = playlistUrlObject.tag.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
+
+ // FIXME: backward compatibility introduced in v2.1.0
+ if (files.length === 0) files = videoFiles
+
if (!segmentsSha256UrlObject) {
logger.warn('No segment sha256 URL found in AP playlist object.', { playlistUrl: playlistUrlObject })
continue
type: VideoStreamingPlaylistType.HLS,
playlistUrl: playlistUrlObject.href,
segmentsSha256Url: segmentsSha256UrlObject.href,
- p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrlObject.href, videoFiles),
+ p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrlObject.href, files),
p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
- videoId: video.id
+ videoId: video.id,
+ tagAPObject: playlistUrlObject.tag
}
attributes.push(attribute)
import { CONFIG } from '../initializers/config'
import { sequelizeTypescript } from '../initializers/database'
import { MVideoWithFile } from '@server/typings/models'
+import { getVideoFilename, getVideoFilePath } from './video-paths'
async function updateStreamingPlaylistsInfohashesIfNeeded () {
const playlistsToUpdate = await VideoStreamingPlaylistModel.listByIncorrectPeerVersion()
const directory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
const masterPlaylists: string[] = [ '#EXTM3U', '#EXT-X-VERSION:3' ]
const masterPlaylistPath = join(directory, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename())
+ const streamingPlaylist = video.getHLSPlaylist()
- for (const file of video.VideoFiles) {
+ for (const file of streamingPlaylist.VideoFiles) {
// If we did not generated a playlist for this resolution, skip
const filePlaylistPath = join(directory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution))
if (await pathExists(filePlaylistPath) === false) continue
- const videoFilePath = video.getVideoFilePath(file)
+ const videoFilePath = getVideoFilePath(streamingPlaylist, file)
const size = await getVideoFileSize(videoFilePath)
const json: { [filename: string]: { [range: string]: string } } = {}
const playlistDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
+ const hlsPlaylist = video.getHLSPlaylist()
// For all the resolutions available for this video
- for (const file of video.VideoFiles) {
+ for (const file of hlsPlaylist.VideoFiles) {
const rangeHashes: { [range: string]: string } = {}
- const videoPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, file.resolution))
+ const videoPath = getVideoFilePath(hlsPlaylist, file)
const playlistPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution))
// Maybe the playlist is not generated for this resolution yet
}
await close(fd)
- const videoFilename = VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, file.resolution)
+ const videoFilename = getVideoFilename(hlsPlaylist, file)
json[videoFilename] = rangeHashes
}
import { VideoFileModel } from '../../../models/video/video-file'
import { extname } from 'path'
import { MVideoFile, MVideoWithFile } from '@server/typings/models'
+import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
+import { getVideoFilePath } from '@server/lib/video-paths'
export type VideoFileImportPayload = {
videoUUID: string,
updatedVideoFile = currentVideoFile
}
- const outputPath = video.getVideoFilePath(updatedVideoFile)
+ const outputPath = getVideoFilePath(video, updatedVideoFile)
await copy(inputFilePath, outputPath)
- await video.createTorrentAndSetInfoHash(updatedVideoFile)
+ await createTorrentAndSetInfoHash(video, updatedVideoFile)
await updatedVideoFile.save()
import { VideoImportModel } from '../../../models/video/video-import'
import { VideoImportState } from '../../../../shared/models/videos'
import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
-import { extname, join } from 'path'
+import { extname } from 'path'
import { VideoFileModel } from '../../../models/video/video-file'
import { VIDEO_IMPORT_TIMEOUT } from '../../../initializers/constants'
import { VideoState } from '../../../../shared'
import { JobQueue } from '../index'
import { federateVideoIfNeeded } from '../../activitypub'
import { VideoModel } from '../../../models/video/video'
-import { downloadWebTorrentVideo } from '../../../helpers/webtorrent'
+import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent'
import { getSecureTorrentName } from '../../../helpers/utils'
import { move, remove, stat } from 'fs-extra'
import { Notifier } from '../../notifier'
import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
import { MThumbnail } from '../../../typings/models/video/thumbnail'
import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/typings/models/video/video-import'
-import { MVideoBlacklistVideo, MVideoBlacklist } from '@server/typings/models'
+import { getVideoFilePath } from '@server/lib/video-paths'
type VideoImportYoutubeDLPayload = {
type: 'youtube-dl'
}
videoFile = new VideoFileModel(videoFileData)
- const videoWithFiles = Object.assign(videoImport.Video, { VideoFiles: [ videoFile ] })
+ const videoWithFiles = Object.assign(videoImport.Video, { VideoFiles: [ videoFile ], VideoStreamingPlaylists: [] })
// To clean files if the import fails
const videoImportWithFiles: MVideoImportDefaultFiles = Object.assign(videoImport, { Video: videoWithFiles })
// Move file
- videoDestFile = join(CONFIG.STORAGE.VIDEOS_DIR, videoImportWithFiles.Video.getVideoFilename(videoFile))
+ videoDestFile = getVideoFilePath(videoImportWithFiles.Video, videoFile)
await move(tempVideoPath, videoDestFile)
tempVideoPath = null // This path is not used anymore
}
// Create torrent
- await videoImportWithFiles.Video.createTorrentAndSetInfoHash(videoFile)
+ await createTorrentAndSetInfoHash(videoImportWithFiles.Video, videoFile)
const { videoImportUpdated, video } = await sequelizeTypescript.transaction(async t => {
const videoImportToUpdate = videoImportWithFiles as MVideoImportVideo
import * as Bull from 'bull'
-import { VideoResolution, VideoState } from '../../../../shared'
+import { VideoResolution } from '../../../../shared'
import { logger } from '../../../helpers/logger'
import { VideoModel } from '../../../models/video/video'
import { JobQueue } from '../job-queue'
import { sequelizeTypescript } from '../../../initializers'
import * as Bluebird from 'bluebird'
import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils'
-import { generateHlsPlaylist, optimizeVideofile, transcodeOriginalVideofile, mergeAudioVideofile } from '../../video-transcoding'
+import { generateHlsPlaylist, mergeAudioVideofile, optimizeOriginalVideofile, transcodeNewResolution } from '../../video-transcoding'
import { Notifier } from '../../notifier'
import { CONFIG } from '../../../initializers/config'
-import { MVideoUUID, MVideoWithFile } from '@server/typings/models'
+import { MVideoFullLight, MVideoUUID, MVideoWithFile } from '@server/typings/models'
interface BaseTranscodingPayload {
videoUUID: string
type: 'hls'
isPortraitMode?: boolean
resolution: VideoResolution
+ copyCodecs: boolean
}
interface NewResolutionTranscodingPayload extends BaseTranscodingPayload {
}
if (payload.type === 'hls') {
- await generateHlsPlaylist(video, payload.resolution, payload.isPortraitMode || false)
+ await generateHlsPlaylist(video, payload.resolution, payload.copyCodecs, payload.isPortraitMode || false)
await retryTransactionWrapper(onHlsPlaylistGenerationSuccess, video)
} else if (payload.type === 'new-resolution') {
- await transcodeOriginalVideofile(video, payload.resolution, payload.isPortraitMode || false)
+ await transcodeNewResolution(video, payload.resolution, payload.isPortraitMode || false)
await retryTransactionWrapper(publishNewResolutionIfNeeded, video, payload)
} else if (payload.type === 'merge-audio') {
await retryTransactionWrapper(publishNewResolutionIfNeeded, video, payload)
} else {
- await optimizeVideofile(video)
+ await optimizeOriginalVideofile(video)
await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload)
}
return video
}
-async function onHlsPlaylistGenerationSuccess (video: MVideoUUID) {
+async function onHlsPlaylistGenerationSuccess (video: MVideoFullLight) {
if (video === undefined) return undefined
- await sequelizeTypescript.transaction(async t => {
- // Maybe the video changed in database, refresh it
- let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t)
- // Video does not exist anymore
- if (!videoDatabase) return undefined
-
- // If the video was not published, we consider it is a new one for other instances
- await federateVideoIfNeeded(videoDatabase, false, t)
- })
-}
-
-async function publishNewResolutionIfNeeded (video: MVideoUUID, payload?: NewResolutionTranscodingPayload | MergeAudioTranscodingPayload) {
- const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => {
- // Maybe the video changed in database, refresh it
- let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t)
- // Video does not exist anymore
- if (!videoDatabase) return undefined
-
- let videoPublished = false
-
- // We transcoded the video file in another format, now we can publish it
- if (videoDatabase.state !== VideoState.PUBLISHED) {
- videoPublished = true
-
- videoDatabase.state = VideoState.PUBLISHED
- videoDatabase.publishedAt = new Date()
- videoDatabase = await videoDatabase.save({ transaction: t })
+ // We generated the HLS playlist, we don't need the webtorrent files anymore if the admin disabled it
+ if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false) {
+ for (const file of video.VideoFiles) {
+ await video.removeFile(file)
+ await file.destroy()
}
- // If the video was not published, we consider it is a new one for other instances
- await federateVideoIfNeeded(videoDatabase, videoPublished, t)
+ video.VideoFiles = []
+ }
- return { videoDatabase, videoPublished }
- })
+ return publishAndFederateIfNeeded(video)
+}
- if (videoPublished) {
- Notifier.Instance.notifyOnNewVideoIfNeeded(videoDatabase)
- Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase)
- }
+async function publishNewResolutionIfNeeded (video: MVideoUUID, payload?: NewResolutionTranscodingPayload | MergeAudioTranscodingPayload) {
+ await publishAndFederateIfNeeded(video)
await createHlsJobIfEnabled(payload)
}
if (videoArg === undefined) return undefined
// Outside the transaction (IO on disk)
- const { videoFileResolution } = await videoArg.getOriginalFileResolution()
+ const { videoFileResolution } = await videoArg.getMaxQualityResolution()
const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => {
// Maybe the video changed in database, refresh it
let videoPublished = false
+ const hlsPayload = Object.assign({}, payload, { resolution: videoDatabase.getMaxQualityFile().resolution })
+ await createHlsJobIfEnabled(hlsPayload)
+
if (resolutionsEnabled.length !== 0) {
const tasks: (Bluebird<Bull.Job<any>> | Promise<Bull.Job<any>>)[] = []
for (const resolution of resolutionsEnabled) {
- const dataInput = {
- type: 'new-resolution' as 'new-resolution',
- videoUUID: videoDatabase.uuid,
- resolution
+ let dataInput: VideoTranscodingPayload
+
+ if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED) {
+ dataInput = {
+ type: 'new-resolution' as 'new-resolution',
+ videoUUID: videoDatabase.uuid,
+ resolution
+ }
+ } else if (CONFIG.TRANSCODING.HLS.ENABLED) {
+ dataInput = {
+ type: 'hls',
+ videoUUID: videoDatabase.uuid,
+ resolution,
+ isPortraitMode: false,
+ copyCodecs: false
+ }
}
const p = JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput })
logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled })
} else {
- videoPublished = true
-
// No transcoding to do, it's now published
- videoDatabase.state = VideoState.PUBLISHED
- videoDatabase = await videoDatabase.save({ transaction: t })
+ videoPublished = await videoDatabase.publishIfNeededAndSave(t)
logger.info('No transcoding jobs created for video %s (no resolutions).', videoDatabase.uuid, { privacy: videoDatabase.privacy })
}
if (payload.isNewVideo) Notifier.Instance.notifyOnNewVideoIfNeeded(videoDatabase)
if (videoPublished) Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase)
-
- const hlsPayload = Object.assign({}, payload, { resolution: videoDatabase.getOriginalFile().resolution })
- await createHlsJobIfEnabled(hlsPayload)
}
// ---------------------------------------------------------------------------
type: 'hls' as 'hls',
videoUUID: payload.videoUUID,
resolution: payload.resolution,
- isPortraitMode: payload.isPortraitMode
+ isPortraitMode: payload.isPortraitMode,
+ copyCodecs: true
}
return JobQueue.Instance.createJob({ type: 'video-transcoding', payload: hlsTranscodingPayload })
}
}
+
+async function publishAndFederateIfNeeded (video: MVideoUUID) {
+ const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => {
+ // Maybe the video changed in database, refresh it
+ const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t)
+ // Video does not exist anymore
+ if (!videoDatabase) return undefined
+
+ // We transcoded the video file in another format, now we can publish it
+ const videoPublished = await videoDatabase.publishIfNeededAndSave(t)
+
+ // If the video was not published, we consider it is a new one for other instances
+ await federateVideoIfNeeded(videoDatabase, videoPublished, t)
+
+ return { videoDatabase, videoPublished }
+ })
+
+ if (videoPublished) {
+ Notifier.Instance.notifyOnNewVideoIfNeeded(videoDatabase)
+ Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase)
+ }
+}
import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
import { VideoPrivacy } from '../../../shared/models/videos'
import { Notifier } from '../notifier'
-import { VideoModel } from '../../models/video/video'
import { sequelizeTypescript } from '../../initializers/database'
+import { MVideoFullLight } from '@server/typings/models'
export class UpdateVideosScheduler extends AbstractScheduler {
const publishedVideos = await sequelizeTypescript.transaction(async t => {
const schedules = await ScheduleVideoUpdateModel.listVideosToUpdate(t)
- const publishedVideos: VideoModel[] = []
+ const publishedVideos: MVideoFullLight[] = []
for (const schedule of schedules) {
const video = schedule.Video
await federateVideoIfNeeded(video, isNewVideo, t)
if (oldPrivacy === VideoPrivacy.UNLISTED || oldPrivacy === VideoPrivacy.PRIVATE) {
- video.ScheduleVideoUpdate = schedule
- publishedVideos.push(video)
+ const videoToPublish: MVideoFullLight = Object.assign(video, { ScheduleVideoUpdate: schedule, UserVideoHistories: [] })
+ publishedVideos.push(videoToPublish)
}
}
import { logger } from '../../helpers/logger'
import { VideosRedundancy } from '../../../shared/models/redundancy'
import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
-import { downloadWebTorrentVideo } from '../../helpers/webtorrent'
+import { downloadWebTorrentVideo, generateMagnetUri } from '../../helpers/webtorrent'
import { join } from 'path'
import { move } from 'fs-extra'
import { getServerActor } from '../../helpers/utils'
MVideoRedundancyVideo,
MVideoWithAllFiles
} from '@server/typings/models'
+import { getVideoFilename } from '../video-paths'
type CandidateToDuplicate = {
redundancy: VideosRedundancy,
logger.info('Duplicating %s - %d in videos redundancy with "%s" strategy.', video.url, file.resolution, redundancy.strategy)
const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
- const magnetUri = video.generateMagnetUri(file, baseUrlHttp, baseUrlWs)
+ const magnetUri = await generateMagnetUri(video, file, baseUrlHttp, baseUrlWs)
const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT)
- const destPath = join(CONFIG.STORAGE.REDUNDANCY_DIR, video.getVideoFilename(file))
+ const destPath = join(CONFIG.STORAGE.REDUNDANCY_DIR, getVideoFilename(video, file))
await move(tmpPath, destPath, { overwrite: true })
const createdModel: MVideoRedundancyFileVideo = await VideoRedundancyModel.create({
import { MVideoPlaylistThumbnail } from '../typings/models/video/video-playlist'
import { MVideoFile, MVideoThumbnail } from '../typings/models'
import { MThumbnail } from '../typings/models/video/thumbnail'
+import { getVideoFilePath } from './video-paths'
type ImageSize = { height: number, width: number }
}
function generateVideoMiniature (video: MVideoThumbnail, videoFile: MVideoFile, type: ThumbnailType) {
- const input = video.getVideoFilePath(videoFile)
+ const input = getVideoFilePath(video, videoFile)
const { filename, basePath, height, width, existingThumbnail, outputPath } = buildMetadataFromVideo(video, type)
const thumbnailCreator = videoFile.isAudio()
--- /dev/null
+import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile } from '@server/typings/models'
+import { extractVideo } from './videos'
+import { join } from 'path'
+import { CONFIG } from '@server/initializers/config'
+import { HLS_STREAMING_PLAYLIST_DIRECTORY } from '@server/initializers/constants'
+
+// ################## Video file name ##################
+
+function getVideoFilename (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile) {
+ const video = extractVideo(videoOrPlaylist)
+
+ if (isStreamingPlaylist(videoOrPlaylist)) {
+ return generateVideoStreamingPlaylistName(video.uuid, videoFile.resolution)
+ }
+
+ return generateWebTorrentVideoName(video.uuid, videoFile.resolution, videoFile.extname)
+}
+
+function generateVideoStreamingPlaylistName (uuid: string, resolution: number) {
+ return `${uuid}-${resolution}-fragmented.mp4`
+}
+
+function generateWebTorrentVideoName (uuid: string, resolution: number, extname: string) {
+ return uuid + '-' + resolution + extname
+}
+
+function getVideoFilePath (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile, isRedundancy = false) {
+ if (isStreamingPlaylist(videoOrPlaylist)) {
+ const video = extractVideo(videoOrPlaylist)
+ return join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid, getVideoFilename(videoOrPlaylist, videoFile))
+ }
+
+ const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR
+ return join(baseDir, getVideoFilename(videoOrPlaylist, videoFile))
+}
+
+// ################## Torrents ##################
+
+function getTorrentFileName (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile) {
+ const video = extractVideo(videoOrPlaylist)
+ const extension = '.torrent'
+
+ if (isStreamingPlaylist(videoOrPlaylist)) {
+ return `${video.uuid}-${videoFile.resolution}-${videoOrPlaylist.getStringType()}${extension}`
+ }
+
+ return video.uuid + '-' + videoFile.resolution + extension
+}
+
+function getTorrentFilePath (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile) {
+ return join(CONFIG.STORAGE.TORRENTS_DIR, getTorrentFileName(videoOrPlaylist, videoFile))
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ generateVideoStreamingPlaylistName,
+ generateWebTorrentVideoName,
+ getVideoFilename,
+ getVideoFilePath,
+
+ getTorrentFileName,
+ getTorrentFilePath
+}
import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants'
-import { basename, join } from 'path'
+import { basename, extname as extnameUtil, join } from 'path'
import {
canDoQuickTranscode,
getDurationFromVideoFile,
import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type'
import { CONFIG } from '../initializers/config'
-import { MVideoFile, MVideoWithFile, MVideoWithFileThumbnail } from '@server/typings/models'
+import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoWithAllFiles, MVideoWithFile } from '@server/typings/models'
+import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
+import { generateVideoStreamingPlaylistName, getVideoFilename, getVideoFilePath } from './video-paths'
/**
* Optimize the original video file and replace it. The resolution is not changed.
*/
-async function optimizeVideofile (video: MVideoWithFile, inputVideoFileArg?: MVideoFile) {
- const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
+async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFileArg?: MVideoFile) {
const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
const newExtname = '.mp4'
- const inputVideoFile = inputVideoFileArg ? inputVideoFileArg : video.getOriginalFile()
- const videoInputPath = join(videosDirectory, video.getVideoFilename(inputVideoFile))
+ const inputVideoFile = inputVideoFileArg || video.getMaxQualityFile()
+ const videoInputPath = getVideoFilePath(video, inputVideoFile)
const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
const transcodeType: TranscodeOptionsType = await canDoQuickTranscode(videoInputPath)
: 'video'
const transcodeOptions: TranscodeOptions = {
- type: transcodeType as any, // FIXME: typing issue
+ type: transcodeType,
inputPath: videoInputPath,
outputPath: videoTranscodedPath,
resolution: inputVideoFile.resolution
// Important to do this before getVideoFilename() to take in account the new file extension
inputVideoFile.extname = newExtname
- const videoOutputPath = video.getVideoFilePath(inputVideoFile)
+ const videoOutputPath = getVideoFilePath(video, inputVideoFile)
await onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
} catch (err) {
/**
* Transcode the original video file to a lower resolution.
*/
-async function transcodeOriginalVideofile (video: MVideoWithFile, resolution: VideoResolution, isPortrait: boolean) {
- const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
+async function transcodeNewResolution (video: MVideoWithFile, resolution: VideoResolution, isPortrait: boolean) {
const transcodeDirectory = CONFIG.STORAGE.TMP_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 videoInputPath = getVideoFilePath(video, video.getMaxQualityFile())
const newVideoFile = new VideoFileModel({
resolution,
size: 0,
videoId: video.id
})
- const videoOutputPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(newVideoFile))
- const videoTranscodedPath = join(transcodeDirectory, video.getVideoFilename(newVideoFile))
+ const videoOutputPath = getVideoFilePath(video, newVideoFile)
+ const videoTranscodedPath = join(transcodeDirectory, getVideoFilename(video, newVideoFile))
const transcodeOptions = {
type: 'video' as 'video',
return onVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath)
}
-async function mergeAudioVideofile (video: MVideoWithFileThumbnail, resolution: VideoResolution) {
- const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
+async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: VideoResolution) {
const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
const newExtname = '.mp4'
- const inputVideoFile = video.getOriginalFile()
+ const inputVideoFile = video.getMaxQualityFile()
- const audioInputPath = join(videosDirectory, video.getVideoFilename(video.getOriginalFile()))
+ const audioInputPath = getVideoFilePath(video, inputVideoFile)
const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
// If the user updates the video preview during transcoding
// Important to do this before getVideoFilename() to take in account the new file extension
inputVideoFile.extname = newExtname
- const videoOutputPath = video.getVideoFilePath(inputVideoFile)
+ const videoOutputPath = getVideoFilePath(video, inputVideoFile)
// ffmpeg generated a new video file, so update the video duration
// See https://trac.ffmpeg.org/ticket/5456
video.duration = await getDurationFromVideoFile(videoTranscodedPath)
return onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
}
-async function generateHlsPlaylist (video: MVideoWithFile, resolution: VideoResolution, isPortraitMode: boolean) {
+async function generateHlsPlaylist (video: MVideoWithFile, resolution: VideoResolution, copyCodecs: boolean, isPortraitMode: boolean) {
const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
await ensureDir(join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid))
- const videoInputPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(video.getFile(resolution)))
+ const videoFileInput = copyCodecs
+ ? video.getWebTorrentFile(resolution)
+ : video.getMaxQualityFile()
+
+ const videoOrStreamingPlaylist = videoFileInput.getVideoOrStreamingPlaylist()
+ const videoInputPath = getVideoFilePath(videoOrStreamingPlaylist, videoFileInput)
+
const outputPath = join(baseHlsDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution))
+ const videoFilename = generateVideoStreamingPlaylistName(video.uuid, resolution)
const transcodeOptions = {
type: 'hls' as 'hls',
inputPath: videoInputPath,
outputPath,
resolution,
+ copyCodecs,
isPortraitMode,
hlsPlaylist: {
- videoFilename: VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, resolution)
+ videoFilename
}
}
- await transcode(transcodeOptions)
+ logger.debug('Will run transcode.', { transcodeOptions })
- await updateMasterHLSPlaylist(video)
- await updateSha256Segments(video)
+ await transcode(transcodeOptions)
const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid)
- await VideoStreamingPlaylistModel.upsert({
+ const [ videoStreamingPlaylist ] = await VideoStreamingPlaylistModel.upsert({
videoId: video.id,
playlistUrl,
segmentsSha256Url: WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid),
p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
type: VideoStreamingPlaylistType.HLS
+ }, { returning: true }) as [ MStreamingPlaylistFilesVideo, boolean ]
+ videoStreamingPlaylist.Video = video
+
+ const newVideoFile = new VideoFileModel({
+ resolution,
+ extname: extnameUtil(videoFilename),
+ size: 0,
+ fps: -1,
+ videoStreamingPlaylistId: videoStreamingPlaylist.id
})
+
+ const videoFilePath = getVideoFilePath(videoStreamingPlaylist, newVideoFile)
+ const stats = await stat(videoFilePath)
+
+ newVideoFile.size = stats.size
+ newVideoFile.fps = await getVideoFileFPS(videoFilePath)
+
+ await createTorrentAndSetInfoHash(videoStreamingPlaylist, newVideoFile)
+
+ const updatedVideoFile = await newVideoFile.save()
+
+ videoStreamingPlaylist.VideoFiles = await videoStreamingPlaylist.$get('VideoFiles') as VideoFileModel[]
+ videoStreamingPlaylist.VideoFiles.push(updatedVideoFile)
+
+ video.setHLSPlaylist(videoStreamingPlaylist)
+
+ await updateMasterHLSPlaylist(video)
+ await updateSha256Segments(video)
+
+ return video
}
// ---------------------------------------------------------------------------
export {
generateHlsPlaylist,
- optimizeVideofile,
- transcodeOriginalVideofile,
+ optimizeOriginalVideofile,
+ transcodeNewResolution,
mergeAudioVideofile
}
videoFile.size = stats.size
videoFile.fps = fps
- await video.createTorrentAndSetInfoHash(videoFile)
+ await createTorrentAndSetInfoHash(video, videoFile)
const updatedVideoFile = await videoFile.save()
--- /dev/null
+import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo } from '@server/typings/models'
+
+function extractVideo (videoOrPlaylist: MVideo | MStreamingPlaylistVideo) {
+ return isStreamingPlaylist(videoOrPlaylist)
+ ? videoOrPlaylist.Video
+ : videoOrPlaylist
+}
+
+export {
+ extractVideo
+}
body('transcoding.resolutions.720p').isBoolean().withMessage('Should have a valid transcoding 720p resolution enabled boolean'),
body('transcoding.resolutions.1080p').isBoolean().withMessage('Should have a valid transcoding 1080p resolution enabled boolean'),
+ body('transcoding.webtorrent.enabled').isBoolean().withMessage('Should have a valid webtorrent transcoding enabled boolean'),
+ body('transcoding.hls.enabled').isBoolean().withMessage('Should have a valid webtorrent transcoding enabled boolean'),
+
body('import.videos.http.enabled').isBoolean().withMessage('Should have a valid import video http enabled boolean'),
body('import.videos.torrent.enabled').isBoolean().withMessage('Should have a valid import video torrent enabled boolean'),
if (areValidationErrors(req, res)) return
if (!checkInvalidConfigIfEmailDisabled(req.body as CustomConfig, res)) return
+ if (!checkInvalidTranscodingConfig(req.body as CustomConfig, res)) return
return next()
}
return true
}
+
+function checkInvalidTranscodingConfig (customConfig: CustomConfig, res: express.Response) {
+ if (customConfig.transcoding.enabled === false) return true
+
+ if (customConfig.transcoding.webtorrent.enabled === false && customConfig.transcoding.hls.enabled === false) {
+ res.status(400)
+ .send({ error: 'You need to enable at least webtorrent transcoding or hls transcoding' })
+ .end()
+ return false
+ }
+
+ return true
+}
const user = res.locals.oauth.token.User
const videoChangeOwnership = res.locals.videoChangeOwnership
- const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getOriginalFile())
+ const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getMaxQualityFile())
if (isAble === false) {
res.status(403)
.json({ error: 'The user video quota is exceeded with this video.' })
expires: this.expiresOn.toISOString(),
url: {
type: 'Link',
- mimeType: 'application/x-mpegURL',
mediaType: 'application/x-mpegURL',
href: this.fileUrl
}
expires: this.expiresOn.toISOString(),
url: {
type: 'Link',
- mimeType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any,
mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any,
href: this.fileUrl,
height: this.VideoFile.resolution,
import { Model, Sequelize } from 'sequelize-typescript'
import * as validator from 'validator'
import { Col } from 'sequelize/types/lib/utils'
-import { col, literal, OrderItem } from 'sequelize'
+import { literal, OrderItem } from 'sequelize'
type SortType = { sortModel: string, sortValue: string }
import { ScopeNames as VideoScopeNames, VideoModel } from './video'
import { VideoPrivacy } from '../../../shared/models/videos'
import { Op, Transaction } from 'sequelize'
-import { MScheduleVideoUpdateFormattable } from '@server/typings/models'
+import { MScheduleVideoUpdateFormattable, MScheduleVideoUpdateVideoAll } from '@server/typings/models'
@Table({
tableName: 'scheduleVideoUpdate',
{
model: VideoModel.scope(
[
- VideoScopeNames.WITH_FILES,
+ VideoScopeNames.WITH_WEBTORRENT_FILES,
+ VideoScopeNames.WITH_STREAMING_PLAYLISTS,
VideoScopeNames.WITH_ACCOUNT_DETAILS,
VideoScopeNames.WITH_BLACKLISTED,
- VideoScopeNames.WITH_THUMBNAILS
+ VideoScopeNames.WITH_THUMBNAILS,
+ VideoScopeNames.WITH_TAGS
]
)
}
transaction: t
}
- return ScheduleVideoUpdateModel.findAll(query)
+ return ScheduleVideoUpdateModel.findAll<MScheduleVideoUpdateVideoAll>(query)
}
static deleteByVideoId (videoId: number, t: Transaction) {
[ScopeNames.WITH_VIDEO]: {
include: [
{
- model: VideoModel.scope([ VideoScopeNames.WITH_THUMBNAILS, VideoScopeNames.WITH_FILES ]),
+ model: VideoModel.scope([
+ VideoScopeNames.WITH_THUMBNAILS,
+ VideoScopeNames.WITH_WEBTORRENT_FILES,
+ VideoScopeNames.WITH_STREAMING_PLAYLISTS
+ ]),
required: true
}
]
import { VideoModel } from './video'
import { VideoRedundancyModel } from '../redundancy/video-redundancy'
import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
-import { FindOptions, QueryTypes, Transaction } from 'sequelize'
+import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize'
import { MIMETYPES } from '../../initializers/constants'
-import { MVideoFile } from '@server/typings/models'
+import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../typings/models/video/video-file'
+import { MStreamingPlaylist, MStreamingPlaylistVideo, MVideo } from '@server/typings/models'
@Table({
tableName: 'videoFile',
indexes: [
{
- fields: [ 'videoId' ]
+ fields: [ 'videoId' ],
+ where: {
+ videoId: {
+ [Op.ne]: null
+ }
+ }
+ },
+ {
+ fields: [ 'videoStreamingPlaylistId' ],
+ where: {
+ videoStreamingPlaylistId: {
+ [Op.ne]: null
+ }
+ }
},
+
{
fields: [ 'infoHash' ]
},
+
{
fields: [ 'videoId', 'resolution', 'fps' ],
- unique: true
+ unique: true,
+ where: {
+ videoId: {
+ [Op.ne]: null
+ }
+ }
+ },
+ {
+ fields: [ 'videoStreamingPlaylistId', 'resolution', 'fps' ],
+ unique: true,
+ where: {
+ videoStreamingPlaylistId: {
+ [Op.ne]: null
+ }
+ }
}
]
})
@BelongsTo(() => VideoModel, {
foreignKey: {
- allowNull: false
+ allowNull: true
},
onDelete: 'CASCADE'
})
Video: VideoModel
+ @ForeignKey(() => VideoStreamingPlaylistModel)
+ @Column
+ videoStreamingPlaylistId: number
+
+ @BelongsTo(() => VideoStreamingPlaylistModel, {
+ foreignKey: {
+ allowNull: true
+ },
+ onDelete: 'CASCADE'
+ })
+ VideoStreamingPlaylist: VideoStreamingPlaylistModel
+
@HasMany(() => VideoRedundancyModel, {
foreignKey: {
allowNull: true
}))
}
+ // 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 baseWhere = {
+ fps: videoFile.fps,
+ resolution: videoFile.resolution
+ }
+
+ if (mode === 'streaming-playlist') Object.assign(baseWhere, { videoStreamingPlaylistId: videoFile.videoStreamingPlaylistId })
+ else Object.assign(baseWhere, { videoId: videoFile.videoId })
+
+ const element = await VideoFileModel.findOne({ where: baseWhere, transaction })
+ if (!element) return videoFile.save({ transaction })
+
+ for (const k of Object.keys(videoFile.toJSON())) {
+ element[k] = videoFile[k]
+ }
+
+ return element.save({ transaction })
+ }
+
+ getVideoOrStreamingPlaylist (this: MVideoFileVideo | MVideoFileStreamingPlaylistVideo): MVideo | MStreamingPlaylistVideo {
+ if (this.videoId) return (this as MVideoFileVideo).Video
+
+ return (this as MVideoFileStreamingPlaylistVideo).VideoStreamingPlaylist
+ }
+
isAudio () {
return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname]
}
hasSameUniqueKeysThan (other: MVideoFile) {
return this.fps === other.fps &&
this.resolution === other.resolution &&
- this.videoId === other.videoId
+ (
+ (this.videoId !== null && this.videoId === other.videoId) ||
+ (this.videoStreamingPlaylistId !== null && this.videoStreamingPlaylistId === other.videoStreamingPlaylistId)
+ )
}
}
-import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
+import { Video, VideoDetails } from '../../../shared/models/videos'
import { VideoModel } from './video'
-import {
- ActivityPlaylistInfohashesObject,
- ActivityPlaylistSegmentHashesObject,
- ActivityUrlObject,
- VideoTorrentObject
-} from '../../../shared/models/activitypub/objects'
+import { ActivityTagObject, ActivityUrlObject, VideoTorrentObject } from '../../../shared/models/activitypub/objects'
import { MIMETYPES, WEBSERVER } from '../../initializers/constants'
import { VideoCaptionModel } from './video-caption'
import {
} from '../../lib/activitypub'
import { isArray } from '../../helpers/custom-validators/misc'
import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model'
-import { MStreamingPlaylistRedundanciesOpt, MVideo, MVideoAP, MVideoFormattable, MVideoFormattableDetails } from '../../typings/models'
-import { MStreamingPlaylistRedundancies } from '../../typings/models/video/video-streaming-playlist'
+import {
+ MStreamingPlaylistRedundanciesOpt,
+ MStreamingPlaylistVideo,
+ MVideo,
+ MVideoAP,
+ MVideoFile,
+ MVideoFormattable,
+ MVideoFormattableDetails
+} from '../../typings/models'
import { MVideoFileRedundanciesOpt } from '../../typings/models/video/video-file'
+import { VideoFile } from '@shared/models/videos/video-file.model'
+import { generateMagnetUri } from '@server/helpers/webtorrent'
export type VideoFormattingJSONOptions = {
completeDescription?: boolean
const tags = video.Tags ? video.Tags.map(t => t.name) : []
- const streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video.VideoStreamingPlaylists)
+ const streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists)
const detailsJson = {
support: video.support,
}
// Format and sort video files
- detailsJson.files = videoFilesModelToFormattedJSON(video, video.VideoFiles)
+ detailsJson.files = videoFilesModelToFormattedJSON(video, baseUrlHttp, baseUrlWs, video.VideoFiles)
return Object.assign(formattedJson, detailsJson)
}
-function streamingPlaylistsModelToFormattedJSON (playlists: MStreamingPlaylistRedundanciesOpt[]): VideoStreamingPlaylist[] {
+function streamingPlaylistsModelToFormattedJSON (video: MVideo, playlists: MStreamingPlaylistRedundanciesOpt[]): VideoStreamingPlaylist[] {
if (isArray(playlists) === false) return []
+ const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
+
return playlists
.map(playlist => {
+ const playlistWithVideo = Object.assign(playlist, { Video: video })
+
const redundancies = isArray(playlist.RedundancyVideos)
? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl }))
: []
+ const files = videoFilesModelToFormattedJSON(playlistWithVideo, baseUrlHttp, baseUrlWs, playlist.VideoFiles)
+
return {
id: playlist.id,
type: playlist.type,
playlistUrl: playlist.playlistUrl,
segmentsSha256Url: playlist.segmentsSha256Url,
- redundancies
- } as VideoStreamingPlaylist
+ redundancies,
+ files
+ }
})
}
-function videoFilesModelToFormattedJSON (video: MVideo, videoFiles: MVideoFileRedundanciesOpt[]): VideoFile[] {
- const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
-
+function videoFilesModelToFormattedJSON (
+ model: MVideo | MStreamingPlaylistVideo,
+ baseUrlHttp: string,
+ baseUrlWs: string,
+ videoFiles: MVideoFileRedundanciesOpt[]
+): VideoFile[] {
return videoFiles
.map(videoFile => {
let resolutionLabel = videoFile.resolution + 'p'
id: videoFile.resolution,
label: resolutionLabel
},
- magnetUri: video.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs),
+ magnetUri: generateMagnetUri(model, 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)
+ torrentUrl: model.getTorrentUrl(videoFile, baseUrlHttp),
+ torrentDownloadUrl: model.getTorrentDownloadUrl(videoFile, baseUrlHttp),
+ fileUrl: model.getVideoFileUrl(videoFile, baseUrlHttp),
+ fileDownloadUrl: model.getVideoFileDownloadUrl(videoFile, baseUrlHttp)
} as VideoFile
})
.sort((a, b) => {
})
}
+function addVideoFilesInAPAcc (
+ acc: ActivityUrlObject[] | ActivityTagObject[],
+ model: MVideoAP | MStreamingPlaylistVideo,
+ baseUrlHttp: string,
+ baseUrlWs: string,
+ files: MVideoFile[]
+) {
+ for (const file of files) {
+ acc.push({
+ type: 'Link',
+ mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any,
+ href: model.getVideoFileUrl(file, baseUrlHttp),
+ height: file.resolution,
+ size: file.size,
+ fps: file.fps
+ })
+
+ acc.push({
+ type: 'Link',
+ mediaType: 'application/x-bittorrent' as 'application/x-bittorrent',
+ href: model.getTorrentUrl(file, baseUrlHttp),
+ height: file.resolution
+ })
+
+ acc.push({
+ type: 'Link',
+ mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
+ href: generateMagnetUri(model, file, baseUrlHttp, baseUrlWs),
+ height: file.resolution
+ })
+ }
+}
+
function videoModelToActivityPubObject (video: MVideoAP): VideoTorrentObject {
const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
if (!video.Tags) video.Tags = []
}
const url: ActivityUrlObject[] = []
- for (const file of video.VideoFiles) {
- url.push({
- type: 'Link',
- mimeType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any,
- mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any,
- href: video.getVideoFileUrl(file, baseUrlHttp),
- height: file.resolution,
- size: file.size,
- fps: file.fps
- })
-
- url.push({
- type: 'Link',
- mimeType: 'application/x-bittorrent' as 'application/x-bittorrent',
- mediaType: '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',
- mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
- href: video.generateMagnetUri(file, baseUrlHttp, baseUrlWs),
- height: file.resolution
- })
- }
+ addVideoFilesInAPAcc(url, video, baseUrlHttp, baseUrlWs, video.VideoFiles || [])
for (const playlist of (video.VideoStreamingPlaylists || [])) {
- let tag: (ActivityPlaylistSegmentHashesObject | ActivityPlaylistInfohashesObject)[]
+ let tag: ActivityTagObject[]
tag = playlist.p2pMediaLoaderInfohashes
.map(i => ({ type: 'Infohash' as 'Infohash', name: i }))
tag.push({
type: 'Link',
name: 'sha256',
- mimeType: 'application/json' as 'application/json',
mediaType: 'application/json' as 'application/json',
href: playlist.segmentsSha256Url
})
+ const playlistWithVideo = Object.assign(playlist, { Video: video })
+ addVideoFilesInAPAcc(tag, playlistWithVideo, baseUrlHttp, baseUrlWs, playlist.VideoFiles || [])
+
url.push({
type: 'Link',
- mimeType: 'application/x-mpegURL' as 'application/x-mpegURL',
mediaType: 'application/x-mpegURL' as 'application/x-mpegURL',
href: playlist.playlistUrl,
tag
// Add video url too
url.push({
type: 'Link',
- mimeType: 'text/html',
mediaType: 'text/html',
href: WEBSERVER.URL + '/videos/watch/' + video.uuid
})
import { VideoRedundancyModel } from '../redundancy/video-redundancy'
import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
-import { CONSTRAINTS_FIELDS, P2P_MEDIA_LOADER_PEER_VERSION, STATIC_PATHS } from '../../initializers/constants'
+import { CONSTRAINTS_FIELDS, P2P_MEDIA_LOADER_PEER_VERSION, STATIC_DOWNLOAD_PATHS, STATIC_PATHS } from '../../initializers/constants'
import { join } from 'path'
import { sha1 } from '../../helpers/core-utils'
import { isArrayOf } from '../../helpers/custom-validators/misc'
import { Op, QueryTypes } from 'sequelize'
import { MStreamingPlaylist, MVideoFile } from '@server/typings/models'
+import { VideoFileModel } from '@server/models/video/video-file'
+import { getTorrentFileName, getVideoFilename } from '@server/lib/video-paths'
@Table({
tableName: 'videoStreamingPlaylist',
})
Video: VideoModel
+ @HasMany(() => VideoFileModel, {
+ foreignKey: {
+ allowNull: true
+ },
+ onDelete: 'CASCADE'
+ })
+ VideoFiles: VideoFileModel[]
+
@HasMany(() => VideoRedundancyModel, {
foreignKey: {
allowNull: false
.then(results => results.length === 1)
}
- static buildP2PMediaLoaderInfoHashes (playlistUrl: string, videoFiles: MVideoFile[]) {
+ static buildP2PMediaLoaderInfoHashes (playlistUrl: string, files: unknown[]) {
const hashes: string[] = []
// https://github.com/Novage/p2p-media-loader/blob/master/p2p-media-loader-core/lib/p2p-media-manager.ts#L115
- for (let i = 0; i < videoFiles.length; i++) {
+ for (let i = 0; i < files.length; i++) {
hashes.push(sha1(`${P2P_MEDIA_LOADER_PEER_VERSION}${playlistUrl}+V${i}`))
}
return 'segments-sha256.json'
}
- static getHlsVideoName (uuid: string, resolution: number) {
- return `${uuid}-${resolution}-fragmented.mp4`
- }
-
static getHlsMasterPlaylistStaticPath (videoUUID: string) {
return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename())
}
return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getStringType() + '/' + this.Video.uuid
}
+ getTorrentDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
+ return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
+ }
+
+ getVideoFileDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
+ return baseUrlHttp + STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + getVideoFilename(this, videoFile)
+ }
+
+ getVideoFileUrl (videoFile: MVideoFile, baseUrlHttp: string) {
+ return baseUrlHttp + join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, this.Video.uuid, getVideoFilename(this, videoFile))
+ }
+
+ getTorrentUrl (videoFile: MVideoFile, baseUrlHttp: string) {
+ return baseUrlHttp + join(STATIC_PATHS.TORRENTS, getTorrentFileName(this, videoFile))
+ }
+
+ getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) {
+ return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
+ }
+
hasSameUniqueKeysThan (other: MStreamingPlaylist) {
return this.type === other.type &&
this.videoId === other.videoId
import * as Bluebird from 'bluebird'
import { maxBy } from 'lodash'
-import * as magnetUtil from 'magnet-uri'
-import * as parseTorrent from 'parse-torrent'
import { join } from 'path'
import {
CountOptions,
} from 'sequelize-typescript'
import { UserRight, VideoPrivacy, VideoState } from '../../../shared'
import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
-import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
+import { Video, VideoDetails } from '../../../shared/models/videos'
import { VideoFilter } from '../../../shared/models/videos/video-query.type'
import { peertubeTruncate } from '../../helpers/core-utils'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
-import { isArray, isBooleanValid } from '../../helpers/custom-validators/misc'
+import { isBooleanValid } from '../../helpers/custom-validators/misc'
import {
isVideoCategoryValid,
isVideoDescriptionValid,
import { ScheduleVideoUpdateModel } from './schedule-video-update'
import { VideoCaptionModel } from './video-caption'
import { VideoBlacklistModel } from './video-blacklist'
-import { remove, writeFile } from 'fs-extra'
+import { remove } from 'fs-extra'
import { VideoViewModel } from './video-views'
import { VideoRedundancyModel } from '../redundancy/video-redundancy'
import {
import { CONFIG } from '../../initializers/config'
import { ThumbnailModel } from './thumbnail'
import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
-import { createTorrentPromise } from '../../helpers/webtorrent'
import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
import {
MChannel,
MChannelAccountDefault,
MChannelId,
+ MStreamingPlaylist,
+ MStreamingPlaylistFilesVideo,
MUserAccountId,
MUserId,
MVideoAccountLight,
MVideoAccountLightBlacklistAllFiles,
MVideoAP,
MVideoDetails,
+ MVideoFileVideo,
MVideoFormattable,
MVideoFormattableDetails,
MVideoForUser,
MVideoWithFile,
MVideoWithRights
} from '../../typings/models'
-import { MVideoFile, MVideoFileRedundanciesOpt } from '../../typings/models/video/video-file'
+import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../typings/models/video/video-file'
import { MThumbnail } from '../../typings/models/video/thumbnail'
+import { VideoFile } from '@shared/models/videos/video-file.model'
+import { getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
const indexes: (ModelIndexesOptions & { where?: WhereOptions })[] = [
FOR_API = 'FOR_API',
WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS',
WITH_TAGS = 'WITH_TAGS',
- WITH_FILES = 'WITH_FILES',
+ WITH_WEBTORRENT_FILES = 'WITH_WEBTORRENT_FILES',
WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
WITH_BLACKLISTED = 'WITH_BLACKLISTED',
WITH_BLOCKLIST = 'WITH_BLOCKLIST',
}
]
},
- [ ScopeNames.WITH_FILES ]: (withRedundancies = false) => {
+ [ ScopeNames.WITH_WEBTORRENT_FILES ]: (withRedundancies = false) => {
let subInclude: any[] = []
if (withRedundancies === true) {
}
},
[ ScopeNames.WITH_STREAMING_PLAYLISTS ]: (withRedundancies = false) => {
- let subInclude: any[] = []
+ const subInclude: IncludeOptions[] = [
+ {
+ model: VideoFileModel.unscoped(),
+ required: false
+ }
+ ]
if (withRedundancies === true) {
- subInclude = [
- {
- attributes: [ 'fileUrl' ],
- model: VideoRedundancyModel.unscoped(),
- required: false
- }
- ]
+ subInclude.push({
+ attributes: [ 'fileUrl' ],
+ model: VideoRedundancyModel.unscoped(),
+ required: false
+ })
}
return {
@HasMany(() => VideoFileModel, {
foreignKey: {
name: 'videoId',
- allowNull: false
+ allowNull: true
},
hooks: true,
onDelete: 'cascade'
}
return VideoModel.scope([
- ScopeNames.WITH_FILES,
+ ScopeNames.WITH_WEBTORRENT_FILES,
ScopeNames.WITH_STREAMING_PLAYLISTS,
ScopeNames.WITH_THUMBNAILS
]).findAll(query)
}
return VideoModel.scope([
- ScopeNames.WITH_FILES,
+ ScopeNames.WITH_WEBTORRENT_FILES,
ScopeNames.WITH_STREAMING_PLAYLISTS,
ScopeNames.WITH_THUMBNAILS
]).findOne(query)
return VideoModel.scope([
ScopeNames.WITH_ACCOUNT_DETAILS,
- ScopeNames.WITH_FILES,
+ ScopeNames.WITH_WEBTORRENT_FILES,
ScopeNames.WITH_STREAMING_PLAYLISTS,
ScopeNames.WITH_THUMBNAILS,
ScopeNames.WITH_BLACKLISTED
ScopeNames.WITH_BLACKLISTED,
ScopeNames.WITH_ACCOUNT_DETAILS,
ScopeNames.WITH_SCHEDULED_UPDATE,
- ScopeNames.WITH_FILES,
+ ScopeNames.WITH_WEBTORRENT_FILES,
ScopeNames.WITH_STREAMING_PLAYLISTS,
ScopeNames.WITH_THUMBNAILS
]
ScopeNames.WITH_ACCOUNT_DETAILS,
ScopeNames.WITH_SCHEDULED_UPDATE,
ScopeNames.WITH_THUMBNAILS,
- { method: [ ScopeNames.WITH_FILES, true ] },
+ { method: [ ScopeNames.WITH_WEBTORRENT_FILES, true ] },
{ method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] }
]
this.VideoChannel.Account.isBlocked()
}
- getOriginalFile <T extends MVideoWithFile> (this: T) {
- if (Array.isArray(this.VideoFiles) === false) return undefined
+ getMaxQualityFile <T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo {
+ if (Array.isArray(this.VideoFiles) && this.VideoFiles.length !== 0) {
+ const file = maxBy(this.VideoFiles, file => file.resolution)
+
+ return Object.assign(file, { Video: this })
+ }
+
+ // No webtorrent files, try with streaming playlist files
+ if (Array.isArray(this.VideoStreamingPlaylists) && this.VideoStreamingPlaylists.length !== 0) {
+ const streamingPlaylistWithVideo = Object.assign(this.VideoStreamingPlaylists[0], { Video: this })
+
+ const file = maxBy(streamingPlaylistWithVideo.VideoFiles, file => file.resolution)
+ return Object.assign(file, { VideoStreamingPlaylist: streamingPlaylistWithVideo })
+ }
- // The original file is the file that have the higher resolution
- return maxBy(this.VideoFiles, file => file.resolution)
+ return undefined
}
- getFile <T extends MVideoWithFile> (this: T, resolution: number) {
+ getWebTorrentFile <T extends MVideoWithFile> (this: T, resolution: number): MVideoFileVideo {
if (Array.isArray(this.VideoFiles) === false) return undefined
- return this.VideoFiles.find(f => f.resolution === resolution)
+ const file = this.VideoFiles.find(f => f.resolution === resolution)
+ if (!file) return undefined
+
+ return Object.assign(file, { Video: this })
}
async addAndSaveThumbnail (thumbnail: MThumbnail, transaction: Transaction) {
this.Thumbnails.push(savedThumbnail)
}
- getVideoFilename (videoFile: MVideoFile) {
- return this.uuid + '-' + videoFile.resolution + videoFile.extname
- }
-
generateThumbnailName () {
return this.uuid + '.jpg'
}
return this.Thumbnails.find(t => t.type === ThumbnailType.PREVIEW)
}
- getTorrentFileName (videoFile: MVideoFile) {
- const extension = '.torrent'
- return this.uuid + '-' + videoFile.resolution + extension
- }
-
isOwned () {
return this.remote === false
}
- getTorrentFilePath (videoFile: MVideoFile) {
- return join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
- }
-
- getVideoFilePath (videoFile: MVideoFile) {
- return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
- }
-
- async createTorrentAndSetInfoHash (videoFile: MVideoFile) {
- const options = {
- // Keep the extname, it's used by the client to stream the file inside a web browser
- name: `${this.name} ${videoFile.resolution}p${videoFile.extname}`,
- createdBy: 'PeerTube',
- announceList: [
- [ WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT + '/tracker/socket' ],
- [ WEBSERVER.URL + '/tracker/announce' ]
- ],
- urlList: [ WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) ]
- }
-
- const torrent = await createTorrentPromise(this.getVideoFilePath(videoFile), options)
-
- const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
- logger.info('Creating torrent %s.', filePath)
-
- await writeFile(filePath, torrent)
-
- const parsedTorrent = parseTorrent(torrent)
- videoFile.infoHash = parsedTorrent.infoHash
- }
-
getWatchStaticPath () {
return '/videos/watch/' + this.uuid
}
}
getFormattedVideoFilesJSON (): VideoFile[] {
- return videoFilesModelToFormattedJSON(this, this.VideoFiles)
+ const { baseUrlHttp, baseUrlWs } = this.getBaseUrls()
+ return videoFilesModelToFormattedJSON(this, baseUrlHttp, baseUrlWs, this.VideoFiles)
}
toActivityPubObject (this: MVideoAP): VideoTorrentObject {
return peertubeTruncate(this.description, { length: maxLength })
}
- getOriginalFileResolution () {
- const originalFilePath = this.getVideoFilePath(this.getOriginalFile())
+ getMaxQualityResolution () {
+ const file = this.getMaxQualityFile()
+ const videoOrPlaylist = file.getVideoOrStreamingPlaylist()
+ const originalFilePath = getVideoFilePath(videoOrPlaylist, file)
return getVideoFileResolution(originalFilePath)
}
return `/api/${API_VERSION}/videos/${this.uuid}/description`
}
- getHLSPlaylist () {
+ getHLSPlaylist (): MStreamingPlaylistFilesVideo {
if (!this.VideoStreamingPlaylists) return undefined
- return this.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
+ const playlist = this.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
+ playlist.Video = this
+
+ return playlist
}
- removeFile (videoFile: MVideoFile, isRedundancy = false) {
- const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR
+ setHLSPlaylist (playlist: MStreamingPlaylist) {
+ const toAdd = [ playlist ] as [ VideoStreamingPlaylistModel ]
- const filePath = join(baseDir, this.getVideoFilename(videoFile))
+ if (Array.isArray(this.VideoStreamingPlaylists) === false || this.VideoStreamingPlaylists.length === 0) {
+ this.VideoStreamingPlaylists = toAdd
+ return
+ }
+
+ this.VideoStreamingPlaylists = this.VideoStreamingPlaylists
+ .filter(s => s.type !== VideoStreamingPlaylistType.HLS)
+ .concat(toAdd)
+ }
+
+ removeFile (videoFile: MVideoFile, isRedundancy = false) {
+ const filePath = getVideoFilePath(this, videoFile, isRedundancy)
return remove(filePath)
.catch(err => logger.warn('Cannot delete file %s.', filePath, { err }))
}
removeTorrent (videoFile: MVideoFile) {
- const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
+ const torrentPath = getTorrentFilePath(this, videoFile)
return remove(torrentPath)
.catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
}
return this.save()
}
- getBaseUrls () {
- let baseUrlHttp
- let baseUrlWs
+ async publishIfNeededAndSave (t: Transaction) {
+ if (this.state !== VideoState.PUBLISHED) {
+ this.state = VideoState.PUBLISHED
+ this.publishedAt = new Date()
+ await this.save({ transaction: t })
- if (this.isOwned()) {
- baseUrlHttp = WEBSERVER.URL
- baseUrlWs = WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT
- } else {
- baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Actor.Server.host
- baseUrlWs = REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Actor.Server.host
+ return true
}
- return { baseUrlHttp, baseUrlWs }
+ return false
}
- generateMagnetUri (videoFile: MVideoFileRedundanciesOpt, baseUrlHttp: string, baseUrlWs: string) {
- const xs = this.getTorrentUrl(videoFile, baseUrlHttp)
- const announce = this.getTrackerUrls(baseUrlHttp, baseUrlWs)
- let urlList = [ this.getVideoFileUrl(videoFile, baseUrlHttp) ]
-
- const redundancies = videoFile.RedundancyVideos
- if (isArray(redundancies)) urlList = urlList.concat(redundancies.map(r => r.fileUrl))
-
- const magnetHash = {
- xs,
- announce,
- urlList,
- infoHash: videoFile.infoHash,
- name: this.name
+ getBaseUrls () {
+ if (this.isOwned()) {
+ return {
+ baseUrlHttp: WEBSERVER.URL,
+ baseUrlWs: WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT
+ }
}
- return magnetUtil.encode(magnetHash)
+ return {
+ baseUrlHttp: REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Actor.Server.host,
+ baseUrlWs: REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Actor.Server.host
+ }
}
getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) {
}
getTorrentUrl (videoFile: MVideoFile, baseUrlHttp: string) {
- return baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile)
+ return baseUrlHttp + STATIC_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
}
getTorrentDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
- return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + this.getTorrentFileName(videoFile)
+ return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
}
getVideoFileUrl (videoFile: MVideoFile, baseUrlHttp: string) {
- return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
+ return baseUrlHttp + STATIC_PATHS.WEBSEED + getVideoFilename(this, videoFile)
}
getVideoRedundancyUrl (videoFile: MVideoFile, baseUrlHttp: string) {
- return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getVideoFilename(videoFile)
+ return baseUrlHttp + STATIC_PATHS.REDUNDANCY + getVideoFilename(this, videoFile)
}
getVideoFileDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
- return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile)
+ return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + getVideoFilename(this, videoFile)
}
getBandwidthBits (videoFile: MVideoFile) {
'1080p': false,
'2160p': false
},
+ webtorrent: {
+ enabled: true
+ },
hls: {
enabled: false
}
})
})
+ it('Should fail with a disabled webtorrent & hls transcoding', async function () {
+ const newUpdateParams = immutableAssign(updateParams, {
+ transcoding: {
+ hls: {
+ enabled: false
+ },
+ webtorrent: {
+ enabled: false
+ }
+ }
+ })
+
+ await makePutBodyRequest({
+ url: server.url,
+ path,
+ fields: newUpdateParams,
+ token: server.accessToken,
+ statusCodeExpected: 400
+ })
+ })
+
it('Should success with the correct parameters', async function () {
await makePutBodyRequest({
url: server.url,
expect(data.transcoding.resolutions['720p']).to.be.true
expect(data.transcoding.resolutions['1080p']).to.be.true
expect(data.transcoding.resolutions['2160p']).to.be.true
+ expect(data.transcoding.webtorrent.enabled).to.be.true
expect(data.transcoding.hls.enabled).to.be.true
expect(data.import.videos.http.enabled).to.be.true
expect(data.transcoding.resolutions['1080p']).to.be.false
expect(data.transcoding.resolutions['2160p']).to.be.false
expect(data.transcoding.hls.enabled).to.be.false
+ expect(data.transcoding.webtorrent.enabled).to.be.true
expect(data.import.videos.http.enabled).to.be.false
expect(data.import.videos.torrent.enabled).to.be.false
'1080p': false,
'2160p': false
},
+ webtorrent: {
+ enabled: true
+ },
hls: {
enabled: false
}
doubleFollow,
flushAndRunMultipleServers,
getPlaylist,
- getVideo,
+ getVideo, makeGetRequest, makeRawRequest,
removeVideo,
ServerInfo,
- setAccessTokensToServers,
+ setAccessTokensToServers, updateCustomSubConfig,
updateVideo,
uploadVideo,
- waitJobs
+ waitJobs, webtorrentAdd
} from '../../../../shared/extra-utils'
import { VideoDetails } from '../../../../shared/models/videos'
import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type'
const expect = chai.expect
-async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string, resolutions = [ 240, 360, 480, 720 ]) {
+async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string, hlsOnly: boolean, resolutions = [ 240, 360, 480, 720 ]) {
for (const server of servers) {
- const res = await getVideo(server.url, videoUUID)
- const videoDetails: VideoDetails = res.body
+ const resVideoDetails = await getVideo(server.url, videoUUID)
+ const videoDetails: VideoDetails = resVideoDetails.body
+ const baseUrl = `http://${videoDetails.account.host}`
expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
const hlsPlaylist = videoDetails.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
expect(hlsPlaylist).to.not.be.undefined
+ const hlsFiles = hlsPlaylist.files
+ expect(hlsFiles).to.have.lengthOf(resolutions.length)
+
+ if (hlsOnly) expect(videoDetails.files).to.have.lengthOf(0)
+ else expect(videoDetails.files).to.have.lengthOf(resolutions.length)
+
+ for (const resolution of resolutions) {
+ const file = hlsFiles.find(f => f.resolution.id === resolution)
+ expect(file).to.not.be.undefined
+
+ expect(file.magnetUri).to.have.lengthOf.above(2)
+ expect(file.torrentUrl).to.equal(`${baseUrl}/static/torrents/${videoDetails.uuid}-${file.resolution.id}-hls.torrent`)
+ expect(file.fileUrl).to.equal(`${baseUrl}/static/streaming-playlists/hls/${videoDetails.uuid}/${videoDetails.uuid}-${file.resolution.id}-fragmented.mp4`)
+ expect(file.resolution.label).to.equal(resolution + 'p')
+
+ await makeRawRequest(file.torrentUrl, 200)
+ await makeRawRequest(file.fileUrl, 200)
+
+ const torrent = await webtorrentAdd(file.magnetUri, true)
+ expect(torrent.files).to.be.an('array')
+ expect(torrent.files.length).to.equal(1)
+ expect(torrent.files[0].path).to.exist.and.to.not.equal('')
+ }
+
{
- const res2 = await getPlaylist(hlsPlaylist.playlistUrl)
+ const res = await getPlaylist(hlsPlaylist.playlistUrl)
- const masterPlaylist = res2.text
+ const masterPlaylist = res.text
for (const resolution of resolutions) {
expect(masterPlaylist).to.match(new RegExp('#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',FRAME-RATE=\\d+'))
{
for (const resolution of resolutions) {
- const res2 = await getPlaylist(`http://localhost:${servers[0].port}/static/streaming-playlists/hls/${videoUUID}/${resolution}.m3u8`)
+ const res = await getPlaylist(`${baseUrl}/static/streaming-playlists/hls/${videoUUID}/${resolution}.m3u8`)
- const subPlaylist = res2.text
+ const subPlaylist = res.text
expect(subPlaylist).to.contain(`${videoUUID}-${resolution}-fragmented.mp4`)
}
}
{
- const baseUrl = 'http://localhost:' + servers[0].port + '/static/streaming-playlists/hls'
+ const baseUrlAndPath = baseUrl + '/static/streaming-playlists/hls'
for (const resolution of resolutions) {
- await checkSegmentHash(baseUrl, baseUrl, videoUUID, resolution, hlsPlaylist)
+ await checkSegmentHash(baseUrlAndPath, baseUrlAndPath, videoUUID, resolution, hlsPlaylist)
}
}
}
let videoUUID = ''
let videoAudioUUID = ''
- before(async function () {
- this.timeout(120000)
+ function runTestSuite (hlsOnly: boolean) {
+ it('Should upload a video and transcode it to HLS', async function () {
+ this.timeout(120000)
- const configOverride = {
- transcoding: {
- enabled: true,
- allow_audio_files: true,
- hls: {
- enabled: true
- }
- }
- }
- servers = await flushAndRunMultipleServers(2, configOverride)
+ const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video 1', fixture: 'video_short.webm' })
+ videoUUID = res.body.video.uuid
- // Get the access tokens
- await setAccessTokensToServers(servers)
+ await waitJobs(servers)
- // Server 1 and server 2 follow each other
- await doubleFollow(servers[0], servers[1])
- })
+ await checkHlsPlaylist(servers, videoUUID, hlsOnly)
+ })
- it('Should upload a video and transcode it to HLS', async function () {
- this.timeout(120000)
+ it('Should upload an audio file and transcode it to HLS', async function () {
+ this.timeout(120000)
- const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video 1', fixture: 'video_short.webm' })
- videoUUID = res.body.video.uuid
+ const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video audio', fixture: 'sample.ogg' })
+ videoAudioUUID = res.body.video.uuid
- await waitJobs(servers)
+ await waitJobs(servers)
- await checkHlsPlaylist(servers, videoUUID)
- })
+ await checkHlsPlaylist(servers, videoAudioUUID, hlsOnly, [ DEFAULT_AUDIO_RESOLUTION ])
+ })
- it('Should upload an audio file and transcode it to HLS', async function () {
- this.timeout(120000)
+ it('Should update the video', async function () {
+ this.timeout(10000)
- const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video audio', fixture: 'sample.ogg' })
- videoAudioUUID = res.body.video.uuid
+ await updateVideo(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID, { name: 'video 1 updated' })
- await waitJobs(servers)
+ await waitJobs(servers)
- await checkHlsPlaylist(servers, videoAudioUUID, [ DEFAULT_AUDIO_RESOLUTION ])
- })
+ await checkHlsPlaylist(servers, videoUUID, hlsOnly)
+ })
- it('Should update the video', async function () {
- this.timeout(10000)
+ it('Should delete videos', async function () {
+ this.timeout(10000)
- await updateVideo(servers[0].url, servers[0].accessToken, videoUUID, { name: 'video 1 updated' })
+ await removeVideo(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID)
+ await removeVideo(servers[ 0 ].url, servers[ 0 ].accessToken, videoAudioUUID)
- await waitJobs(servers)
+ await waitJobs(servers)
- await checkHlsPlaylist(servers, videoUUID)
- })
+ for (const server of servers) {
+ await getVideo(server.url, videoUUID, 404)
+ await getVideo(server.url, videoAudioUUID, 404)
+ }
+ })
- it('Should delete videos', async function () {
- this.timeout(10000)
+ it('Should have the playlists/segment deleted from the disk', async function () {
+ for (const server of servers) {
+ await checkDirectoryIsEmpty(server, 'videos')
+ await checkDirectoryIsEmpty(server, join('streaming-playlists', 'hls'))
+ }
+ })
- await removeVideo(servers[0].url, servers[0].accessToken, videoUUID)
- await removeVideo(servers[0].url, servers[0].accessToken, videoAudioUUID)
+ it('Should have an empty tmp directory', async function () {
+ for (const server of servers) {
+ await checkTmpIsEmpty(server)
+ }
+ })
+ }
- await waitJobs(servers)
+ before(async function () {
+ this.timeout(120000)
- for (const server of servers) {
- await getVideo(server.url, videoUUID, 404)
- await getVideo(server.url, videoAudioUUID, 404)
+ const configOverride = {
+ transcoding: {
+ enabled: true,
+ allow_audio_files: true,
+ hls: {
+ enabled: true
+ }
+ }
}
+ servers = await flushAndRunMultipleServers(2, configOverride)
+
+ // Get the access tokens
+ await setAccessTokensToServers(servers)
+
+ // Server 1 and server 2 follow each other
+ await doubleFollow(servers[0], servers[1])
})
- it('Should have the playlists/segment deleted from the disk', async function () {
- for (const server of servers) {
- await checkDirectoryIsEmpty(server, 'videos')
- await checkDirectoryIsEmpty(server, join('streaming-playlists', 'hls'))
- }
+ describe('With WebTorrent & HLS enabled', function () {
+ runTestSuite(false)
})
- it('Should have an empty tmp directory', async function () {
- for (const server of servers) {
- await checkTmpIsEmpty(server)
- }
+ describe('With only HLS enabled', function () {
+
+ before(async function () {
+ await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
+ transcoding: {
+ enabled: true,
+ allowAudioFiles: true,
+ resolutions: {
+ '240p': true,
+ '360p': true,
+ '480p': true,
+ '720p': true,
+ '1080p': true,
+ '2160p': true
+ },
+ hls: {
+ enabled: true
+ },
+ webtorrent: {
+ enabled: false
+ }
+ }
+ })
+ })
+
+ runTestSuite(true)
})
after(async function () {
import 'mocha'
import * as chai from 'chai'
-import { VideoDetails, VideoFile } from '../../../shared/models/videos'
+import { VideoDetails } from '../../../shared/models/videos'
import {
cleanupTests,
doubleFollow,
execCLI,
flushAndRunMultipleServers,
- flushTests,
getEnvCli,
getVideo,
getVideosList,
- killallServers,
ServerInfo,
setAccessTokensToServers,
uploadVideo
} from '../../../shared/extra-utils'
import { waitJobs } from '../../../shared/extra-utils/server/jobs'
+import { VideoFile } from '@shared/models/videos/video-file.model'
const expect = chai.expect
} from './actor'
import { FunctionProperties, PickWith } from '../../utils'
import { MAccountBlocklistId } from './account-blocklist'
-import { MChannelDefault } from '@server/typings/models'
+import { MChannelDefault } from '../video/video-channels'
type Use<K extends keyof AccountModel, M> = PickWith<AccountModel, K, M>
import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import {
MActor,
- MActorAccount,
- MActorDefaultAccountChannel,
MActorChannelAccountActor,
MActorDefault,
+ MActorDefaultAccountChannel,
MActorFormattable,
MActorHost,
MActorUsername
} from './actor'
import { PickWith } from '../../utils'
import { ActorModel } from '@server/models/activitypub/actor'
-import { MChannelDefault } from '@server/typings/models'
+import { MChannelDefault } from '../video/video-channels'
type Use<K extends keyof ActorFollowModel, M> = PickWith<ActorFollowModel, K, M>
+++ /dev/null
-export * from './account'
-export * from './account-blocklist'
-export * from './actor'
-export * from './actor-follow'
-export * from './avatar'
--- /dev/null
+export * from './account'
+export * from './account-blocklist'
+export * from './actor'
+export * from './actor-follow'
+export * from './avatar'
+++ /dev/null
-export * from './account'
-export * from './oauth'
-export * from './server'
-export * from './user'
-export * from './video'
--- /dev/null
+export * from './account'
+export * from './oauth'
+export * from './server'
+export * from './user'
+export * from './video'
+++ /dev/null
-export * from './oauth-client'
-export * from './oauth-token'
--- /dev/null
+export * from './oauth-client'
+export * from './oauth-token'
import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
import { PickWith } from '@server/typings/utils'
-import { MUserAccountUrl } from '@server/typings/models'
+import { MUserAccountUrl } from '../user/user'
type Use<K extends keyof OAuthTokenModel, M> = PickWith<OAuthTokenModel, K, M>
+++ /dev/null
-export * from './plugin'
-export * from './server'
-export * from './server-blocklist'
--- /dev/null
+export * from './plugin'
+export * from './server'
+export * from './server-blocklist'
import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
import { PickWith } from '@server/typings/utils'
-import { MAccountDefault, MAccountFormattable, MServer, MServerFormattable } from '@server/typings/models'
+import { MAccountDefault, MAccountFormattable } from '../account/account'
+import { MServer, MServerFormattable } from './server'
type Use<K extends keyof ServerBlocklistModel, M> = PickWith<ServerBlocklistModel, K, M>
+++ /dev/null
-export * from './user'
-export * from './user-notification'
-export * from './user-notification-setting'
-export * from './user-video-history'
--- /dev/null
+export * from './user'
+export * from './user-notification'
+export * from './user-notification-setting'
+export * from './user-video-history'
} from '../account'
import { MNotificationSetting, MNotificationSettingFormattable } from './user-notification-setting'
import { AccountModel } from '@server/models/account/account'
-import { MChannelFormattable } from '@server/typings/models'
+import { MChannelFormattable } from '../video/video-channels'
type Use<K extends keyof UserModel, M> = PickWith<UserModel, K, M>
+++ /dev/null
-export * from './schedule-video-update'
-export * from './tag'
-export * from './thumbnail'
-export * from './video'
-export * from './video-abuse'
-export * from './video-blacklist'
-export * from './video-caption'
-export * from './video-change-ownership'
-export * from './video-channels'
-export * from './video-comment'
-export * from './video-file'
-export * from './video-import'
-export * from './video-playlist'
-export * from './video-playlist-element'
-export * from './video-rate'
-export * from './video-redundancy'
-export * from './video-share'
-export * from './video-streaming-playlist'
--- /dev/null
+export * from './schedule-video-update'
+export * from './tag'
+export * from './thumbnail'
+export * from './video'
+export * from './video-abuse'
+export * from './video-blacklist'
+export * from './video-caption'
+export * from './video-change-ownership'
+export * from './video-channels'
+export * from './video-comment'
+export * from './video-file'
+export * from './video-import'
+export * from './video-playlist'
+export * from './video-playlist-element'
+export * from './video-rate'
+export * from './video-redundancy'
+export * from './video-share'
+export * from './video-streaming-playlist'
import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
+import { PickWith } from '@server/typings/utils'
+import { MVideoAPWithoutCaption, MVideoWithBlacklistLight } from './video'
+
+type Use<K extends keyof ScheduleVideoUpdateModel, M> = PickWith<ScheduleVideoUpdateModel, K, M>
+
+// ############################################################################
export type MScheduleVideoUpdate = Omit<ScheduleVideoUpdateModel, 'Video'>
// ############################################################################
+export type MScheduleVideoUpdateVideoAll = MScheduleVideoUpdate &
+ Use<'Video', MVideoAPWithoutCaption & MVideoWithBlacklistLight>
+
// Format for API or AP object
export type MScheduleVideoUpdateFormattable = Pick<MScheduleVideoUpdate, 'updateAt' | 'privacy'>
import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
import { PickWith } from '@server/typings/utils'
-import { MVideo, MVideoFormattable } from '@server/typings/models'
+import { MVideo, MVideoFormattable } from './video'
type Use<K extends keyof VideoBlacklistModel, M> = PickWith<VideoBlacklistModel, K, M>
import { VideoCaptionModel } from '../../../models/video/video-caption'
import { FunctionProperties, PickWith } from '@server/typings/utils'
-import { MVideo, MVideoUUID } from '@server/typings/models'
+import { MVideo, MVideoUUID } from './video'
type Use<K extends keyof VideoCaptionModel, M> = PickWith<VideoCaptionModel, K, M>
import { VideoChangeOwnershipModel } from '@server/models/video/video-change-ownership'
import { PickWith } from '@server/typings/utils'
-import { MAccountDefault, MAccountFormattable, MVideo, MVideoWithFileThumbnail } from '@server/typings/models'
+import { MAccountDefault, MAccountFormattable } from '../account/account'
+import { MVideo, MVideoWithAllFiles } from './video'
type Use<K extends keyof VideoChangeOwnershipModel, M> = PickWith<VideoChangeOwnershipModel, K, M>
export type MVideoChangeOwnershipFull = MVideoChangeOwnership &
Use<'Initiator', MAccountDefault> &
Use<'NextOwner', MAccountDefault> &
- Use<'Video', MVideoWithFileThumbnail>
+ Use<'Video', MVideoWithAllFiles>
// ############################################################################
import { VideoCommentModel } from '../../../models/video/video-comment'
import { PickWith, PickWithOpt } from '../../utils'
-import { MAccountDefault, MAccountFormattable, MAccountUrl, MActorUrl } from '../account'
+import { MAccountDefault, MAccountFormattable, MAccountUrl } from '../account'
import { MVideoAccountLight, MVideoFeed, MVideoIdUrl, MVideoUrl } from './video'
type Use<K extends keyof VideoCommentModel, M> = PickWith<VideoCommentModel, K, M>
import { PickWith, PickWithOpt } from '../../utils'
import { MVideo, MVideoUUID } from './video'
import { MVideoRedundancyFileUrl } from './video-redundancy'
+import { MStreamingPlaylistVideo, MStreamingPlaylist } from './video-streaming-playlist'
type Use<K extends keyof VideoFileModel, M> = PickWith<VideoFileModel, K, M>
// ############################################################################
-export type MVideoFile = Omit<VideoFileModel, 'Video' | 'RedundancyVideos'>
+export type MVideoFile = Omit<VideoFileModel, 'Video' | 'RedundancyVideos' | 'VideoStreamingPlaylist'>
export type MVideoFileVideo = MVideoFile &
Use<'Video', MVideo>
+export type MVideoFileStreamingPlaylist = MVideoFile &
+ Use<'VideoStreamingPlaylist', MStreamingPlaylist>
+
+export type MVideoFileStreamingPlaylistVideo = MVideoFile &
+ Use<'VideoStreamingPlaylist', MStreamingPlaylistVideo>
+
export type MVideoFileVideoUUID = MVideoFile &
Use<'Video', MVideoUUID>
export type MVideoFileRedundanciesOpt = MVideoFile &
PickWithOpt<VideoFileModel, 'RedundancyVideos', MVideoRedundancyFileUrl[]>
+
+export function isStreamingPlaylistFile (file: any): file is MVideoFileStreamingPlaylist {
+ return !!file.videoStreamingPlaylistId
+}
+
+export function isWebtorrentFile (file: any): file is MVideoFileVideo {
+ return !!file.videoId
+}
import { VideoImportModel } from '@server/models/video/video-import'
import { PickWith, PickWithOpt } from '@server/typings/utils'
-import { MUser, MVideo, MVideoAccountLight, MVideoFormattable, MVideoTag, MVideoThumbnail, MVideoWithFile } from '@server/typings/models'
+import { MVideo, MVideoAccountLight, MVideoFormattable, MVideoTag, MVideoThumbnail, MVideoWithFile } from './video'
+import { MUser } from '../user/user'
type Use<K extends keyof VideoImportModel, M> = PickWith<VideoImportModel, K, M>
import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element'
import { PickWith } from '@server/typings/utils'
-import { MVideoFormattable, MVideoPlaylistPrivacy, MVideoThumbnail, MVideoUrl } from '@server/typings/models'
+import { MVideoFormattable, MVideoThumbnail, MVideoUrl } from './video'
+import { MVideoPlaylistPrivacy } from './video-playlist'
type Use<K extends keyof VideoPlaylistElementModel, M> = PickWith<VideoPlaylistElementModel, K, M>
import { AccountVideoRateModel } from '@server/models/account/account-video-rate'
import { PickWith } from '@server/typings/utils'
-import { MAccountAudience, MAccountUrl, MVideo, MVideoFormattable } from '..'
+import { MAccountAudience, MAccountUrl } from '../account/account'
+import { MVideo, MVideoFormattable } from './video'
type Use<K extends keyof AccountVideoRateModel, M> = PickWith<AccountVideoRateModel, K, M>
import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy'
import { PickWith, PickWithOpt } from '@server/typings/utils'
-import { MStreamingPlaylistVideo, MVideoFile, MVideoFileVideo, MVideoUrl } from '@server/typings/models'
-import { VideoStreamingPlaylist } from '../../../../shared/models/videos/video-streaming-playlist.model'
import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
-import { VideoFile } from '../../../../shared/models/videos'
import { VideoFileModel } from '@server/models/video/video-file'
+import { MVideoFile, MVideoFileVideo } from './video-file'
+import { MStreamingPlaylistVideo } from './video-streaming-playlist'
+import { MVideoUrl } from './video'
type Use<K extends keyof VideoRedundancyModel, M> = PickWith<VideoRedundancyModel, K, M>
import { VideoStreamingPlaylistModel } from '../../../models/video/video-streaming-playlist'
import { PickWith, PickWithOpt } from '../../utils'
import { MVideoRedundancyFileUrl } from './video-redundancy'
-import { MVideo, MVideoUrl } from '@server/typings/models'
+import { MVideo } from './video'
+import { MVideoFile } from './video-file'
type Use<K extends keyof VideoStreamingPlaylistModel, M> = PickWith<VideoStreamingPlaylistModel, K, M>
// ############################################################################
-export type MStreamingPlaylist = Omit<VideoStreamingPlaylistModel, 'Video' | 'RedundancyVideos'>
+export type MStreamingPlaylist = Omit<VideoStreamingPlaylistModel, 'Video' | 'RedundancyVideos' | 'VideoFiles'>
+
+export type MStreamingPlaylistFiles = MStreamingPlaylist &
+ Use<'VideoFiles', MVideoFile[]>
export type MStreamingPlaylistVideo = MStreamingPlaylist &
Use<'Video', MVideo>
+export type MStreamingPlaylistFilesVideo = MStreamingPlaylist &
+ Use<'VideoFiles', MVideoFile[]> &
+ Use<'Video', MVideo>
+
export type MStreamingPlaylistRedundancies = MStreamingPlaylist &
+ Use<'VideoFiles', MVideoFile[]> &
Use<'RedundancyVideos', MVideoRedundancyFileUrl[]>
export type MStreamingPlaylistRedundanciesOpt = MStreamingPlaylist &
+ Use<'VideoFiles', MVideoFile[]> &
PickWithOpt<VideoStreamingPlaylistModel, 'RedundancyVideos', MVideoRedundancyFileUrl[]>
+
+export function isStreamingPlaylist (value: MVideo | MStreamingPlaylistVideo): value is MStreamingPlaylistVideo {
+ return !!(value as MStreamingPlaylist).playlistUrl
+}
} from './video-channels'
import { MTag } from './tag'
import { MVideoCaptionLanguage } from './video-caption'
-import { MStreamingPlaylist, MStreamingPlaylistRedundancies, MStreamingPlaylistRedundanciesOpt } from './video-streaming-playlist'
+import { MStreamingPlaylistFiles, MStreamingPlaylistRedundancies, MStreamingPlaylistRedundanciesOpt } from './video-streaming-playlist'
import { MVideoFile, MVideoFileRedundanciesOpt } from './video-file'
import { MThumbnail } from './thumbnail'
import { MVideoBlacklist, MVideoBlacklistLight, MVideoBlacklistUnfederated } from './video-blacklist'
// "With" to not confuse with the VideoFile model
export type MVideoWithFile = MVideo &
- Use<'VideoFiles', MVideoFile[]>
+ Use<'VideoFiles', MVideoFile[]> &
+ Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]>
export type MVideoThumbnail = MVideo &
Use<'Thumbnails', MThumbnail[]>
Use<'VideoCaptions', MVideoCaptionLanguage[]>
export type MVideoWithStreamingPlaylist = MVideo &
- Use<'VideoStreamingPlaylists', MStreamingPlaylist[]>
+ Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]>
// ############################################################################
export type MVideoWithAllFiles = MVideo &
Use<'VideoFiles', MVideoFile[]> &
Use<'Thumbnails', MThumbnail[]> &
- Use<'VideoStreamingPlaylists', MStreamingPlaylist[]>
+ Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]>
export type MVideoAccountLightBlacklistAllFiles = MVideo &
Use<'VideoFiles', MVideoFile[]> &
Use<'Thumbnails', MThumbnail[]> &
- Use<'VideoStreamingPlaylists', MStreamingPlaylist[]> &
+ Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]> &
Use<'VideoChannel', MChannelAccountLight> &
Use<'VideoBlacklist', MVideoBlacklistLight>
Use<'UserVideoHistories', MUserVideoHistoryTime[]> &
Use<'VideoFiles', MVideoFile[]> &
Use<'ScheduleVideoUpdate', MScheduleVideoUpdate> &
- Use<'VideoStreamingPlaylists', MStreamingPlaylist[]>
+ Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]>
// ############################################################################
export type MVideoAP = MVideo &
Use<'Tags', MTag[]> &
Use<'VideoChannel', MChannelAccountLight> &
- Use<'VideoStreamingPlaylists', MStreamingPlaylist[]> &
+ Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]> &
Use<'VideoCaptions', MVideoCaptionLanguage[]> &
Use<'VideoBlacklist', MVideoBlacklistUnfederated> &
- Use<'VideoFiles', MVideoFileRedundanciesOpt[]>
+ Use<'VideoFiles', MVideoFileRedundanciesOpt[]> &
+ Use<'Thumbnails', MThumbnail[]>
export type MVideoAPWithoutCaption = Omit<MVideoAP, 'VideoCaptions'>
'1080p': false,
'2160p': false
},
+ webtorrent: {
+ enabled: true
+ },
hls: {
enabled: false
}
// Transcoding enabled: extension will always be .mp4
if (attributes.files.length > 1) extension = '.mp4'
- const magnetUri = file.magnetUri
expect(file.magnetUri).to.have.lengthOf.above(2)
expect(file.torrentUrl).to.equal(`http://${attributes.account.host}/static/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`)
expect(file.fileUrl).to.equal(`http://${attributes.account.host}/static/webseed/${videoDetails.uuid}-${file.resolution.id}${extension}`)
await testImage(url, attributes.previewfile, videoDetails.previewPath)
}
- const torrent = await webtorrentAdd(magnetUri, true)
+ const torrent = await webtorrentAdd(file.magnetUri, true)
expect(torrent.files).to.be.an('array')
expect(torrent.files.length).to.equal(1)
expect(torrent.files[0].path).to.exist.and.to.not.equal('')
name: string
}
-export interface ActivityTagObject {
- type: 'Hashtag' | 'Mention'
- href?: string
- name: string
-}
-
export interface ActivityIconObject {
type: 'Image'
url: string
export type ActivityVideoUrlObject = {
type: 'Link'
- // TODO: remove mimeType (backward compatibility, introduced in v1.1.0)
- mimeType?: 'video/mp4' | 'video/webm' | 'video/ogg'
mediaType: 'video/mp4' | 'video/webm' | 'video/ogg'
href: string
height: number
export type ActivityPlaylistSegmentHashesObject = {
type: 'Link'
name: 'sha256'
- // TODO: remove mimeType (backward compatibility, introduced in v1.1.0)
- mimeType?: 'application/json'
mediaType: 'application/json'
href: string
}
export type ActivityPlaylistUrlObject = {
type: 'Link'
- // TODO: remove mimeType (backward compatibility, introduced in v1.1.0)
- mimeType?: 'application/x-mpegURL'
mediaType: 'application/x-mpegURL'
href: string
- tag?: (ActivityPlaylistSegmentHashesObject | ActivityPlaylistInfohashesObject)[]
+ tag?: ActivityTagObject[]
}
export type ActivityBitTorrentUrlObject = {
type: 'Link'
- // TODO: remove mimeType (backward compatibility, introduced in v1.1.0)
- mimeType?: 'application/x-bittorrent' | 'application/x-bittorrent;x-scheme-handler/magnet'
mediaType: 'application/x-bittorrent' | 'application/x-bittorrent;x-scheme-handler/magnet'
href: string
height: number
}
+export type ActivityMagnetUrlObject = {
+ type: 'Link'
+ mediaType: 'application/x-bittorrent;x-scheme-handler/magnet'
+ href: string
+ height: number
+}
+
export type ActivityHtmlUrlObject = {
type: 'Link'
- // TODO: remove mimeType (backward compatibility, introduced in v1.1.0)
- mimeType?: 'text/html'
mediaType: 'text/html'
href: string
}
-export type ActivityUrlObject = ActivityVideoUrlObject | ActivityPlaylistUrlObject | ActivityBitTorrentUrlObject | ActivityHtmlUrlObject
+export interface ActivityHashTagObject {
+ type: 'Hashtag' | 'Mention'
+ href?: string
+ name: string
+}
+
+export interface ActivityMentionObject {
+ type: 'Hashtag' | 'Mention'
+ href?: string
+ name: string
+}
+
+export type ActivityTagObject = ActivityPlaylistSegmentHashesObject |
+ ActivityPlaylistInfohashesObject |
+ ActivityVideoUrlObject |
+ ActivityHashTagObject |
+ ActivityMentionObject |
+ ActivityBitTorrentUrlObject |
+ ActivityMagnetUrlObject
+
+export type ActivityUrlObject = ActivityVideoUrlObject |
+ ActivityPlaylistUrlObject |
+ ActivityBitTorrentUrlObject |
+ ActivityMagnetUrlObject |
+ ActivityHtmlUrlObject
export interface ActivityPubAttributedTo {
type: 'Group' | 'Person'
transcoding: {
enabled: boolean
+
allowAdditionalExtensions: boolean
allowAudioFiles: boolean
+
threads: number
resolutions: {
'240p': boolean
'1080p': boolean
'2160p': boolean
}
+
+ webtorrent: {
+ enabled: boolean
+ }
+
hls: {
enabled: boolean
}
enabled: boolean
}
+ webtorrent: {
+ enabled: boolean
+ }
+
enabledResolutions: number[]
}
export * from './video-change-ownership.model'
export * from './video-change-ownership-create.model'
export * from './video-create.model'
+export * from './video-file.model'
export * from './video-privacy.enum'
export * from './video-rate.type'
export * from './video-resolution.enum'
--- /dev/null
+import { VideoConstant, VideoResolution } from '@shared/models'
+
+export interface VideoFile {
+ magnetUri: string
+ resolution: VideoConstant<VideoResolution>
+ size: number // Bytes
+ torrentUrl: string
+ torrentDownloadUrl: string
+ fileUrl: string
+ fileDownloadUrl: string
+ fps: number
+}
import { VideoStreamingPlaylistType } from './video-streaming-playlist.type'
+import { VideoFile } from '@shared/models/videos/video-file.model'
export class VideoStreamingPlaylist {
id: number
redundancies: {
baseUrl: string
}[]
+
+ files: VideoFile[]
}
import { VideoScheduleUpdate } from './video-schedule-update.model'
import { VideoConstant } from './video-constant.model'
import { VideoStreamingPlaylist } from './video-streaming-playlist.model'
-
-export interface VideoFile {
- magnetUri: string
- resolution: VideoConstant<VideoResolution>
- size: number // Bytes
- torrentUrl: string
- torrentDownloadUrl: string
- fileUrl: string
- fileDownloadUrl: string
- fps: number
-}
+import { VideoFile } from './video-file.model'
export interface Video {
id: number
],
"typeRoots": [
"node_modules/sitemap/node_modules/@types",
- "node_modules/@types",
- "server/typings"
+ "node_modules/@types"
],
"baseUrl": "./",
"paths": {
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
-typescript@^3.4.3:
- version "3.6.4"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.4.tgz#b18752bb3792bc1a0281335f7f6ebf1bbfc5b91d"
- integrity sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==
+typescript@^3.7.2:
+ version "3.7.2"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.2.tgz#27e489b95fa5909445e9fef5ee48d81697ad18fb"
+ integrity sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ==
uint64be@^2.0.2:
version "2.0.2"