From 092092969633bbcf6d4891a083ea497a7d5c3154 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 29 Jan 2019 08:37:25 +0100 Subject: Add hls support on server --- server/lib/activitypub/cache-file.ts | 23 ++++++- server/lib/activitypub/send/send-create.ts | 9 +-- server/lib/activitypub/send/send-undo.ts | 3 +- server/lib/activitypub/send/send-update.ts | 2 +- server/lib/activitypub/url.ts | 7 +++ server/lib/activitypub/videos.ts | 97 ++++++++++++++++++++++++++++-- 6 files changed, 124 insertions(+), 17 deletions(-) (limited to 'server/lib/activitypub') diff --git a/server/lib/activitypub/cache-file.ts b/server/lib/activitypub/cache-file.ts index f6f068b45..9a40414bb 100644 --- a/server/lib/activitypub/cache-file.ts +++ b/server/lib/activitypub/cache-file.ts @@ -1,11 +1,28 @@ -import { CacheFileObject } from '../../../shared/index' +import { ActivityPlaylistUrlObject, ActivityVideoUrlObject, CacheFileObject } from '../../../shared/index' import { VideoModel } from '../../models/video/video' import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' import { Transaction } from 'sequelize' +import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject, video: VideoModel, byActor: { id?: number }) { - const url = cacheFileObject.url + if (cacheFileObject.url.mediaType === 'application/x-mpegURL') { + const url = cacheFileObject.url + + const playlist = video.VideoStreamingPlaylists.find(t => t.type === VideoStreamingPlaylistType.HLS) + if (!playlist) throw new Error('Cannot find HLS playlist of video ' + video.url) + + return { + expiresOn: new Date(cacheFileObject.expires), + url: cacheFileObject.id, + fileUrl: url.href, + strategy: null, + videoStreamingPlaylistId: playlist.id, + actorId: byActor.id + } + } + + const url = cacheFileObject.url const videoFile = video.VideoFiles.find(f => { return f.resolution === url.height && f.fps === url.fps }) @@ -15,7 +32,7 @@ function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject return { expiresOn: new Date(cacheFileObject.expires), url: cacheFileObject.id, - fileUrl: cacheFileObject.url.href, + fileUrl: url.href, strategy: null, videoFileId: videoFile.id, actorId: byActor.id diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts index e3fca0a17..605aaba06 100644 --- a/server/lib/activitypub/send/send-create.ts +++ b/server/lib/activitypub/send/send-create.ts @@ -1,6 +1,6 @@ import { Transaction } from 'sequelize' import { ActivityAudience, ActivityCreate } from '../../../../shared/models/activitypub' -import { VideoPrivacy } from '../../../../shared/models/videos' +import { Video, VideoPrivacy } from '../../../../shared/models/videos' import { ActorModel } from '../../../models/activitypub/actor' import { VideoModel } from '../../../models/video/video' import { VideoAbuseModel } from '../../../models/video/video-abuse' @@ -39,17 +39,14 @@ async function sendVideoAbuse (byActor: ActorModel, videoAbuse: VideoAbuseModel, return unicastTo(createActivity, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl) } -async function sendCreateCacheFile (byActor: ActorModel, fileRedundancy: VideoRedundancyModel) { +async function sendCreateCacheFile (byActor: ActorModel, video: VideoModel, fileRedundancy: VideoRedundancyModel) { logger.info('Creating job to send file cache of %s.', fileRedundancy.url) - const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(fileRedundancy.VideoFile.Video.id) - const redundancyObject = fileRedundancy.toActivityPubObject() - return sendVideoRelatedCreateActivity({ byActor, video, url: fileRedundancy.url, - object: redundancyObject + object: fileRedundancy.toActivityPubObject() }) } diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts index bf1b6e117..8976fcbc8 100644 --- a/server/lib/activitypub/send/send-undo.ts +++ b/server/lib/activitypub/send/send-undo.ts @@ -73,7 +73,8 @@ async function sendUndoDislike (byActor: ActorModel, video: VideoModel, t: Trans async function sendUndoCacheFile (byActor: ActorModel, redundancyModel: VideoRedundancyModel, t: Transaction) { logger.info('Creating job to undo cache file %s.', redundancyModel.url) - const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(redundancyModel.VideoFile.Video.id) + const videoId = redundancyModel.getVideo().id + const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) const createActivity = buildCreateActivity(redundancyModel.url, byActor, redundancyModel.toActivityPubObject()) return sendUndoVideoRelatedActivity({ byActor, video, url: redundancyModel.url, activity: createActivity, transaction: t }) diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts index a68f03edf..839f66470 100644 --- a/server/lib/activitypub/send/send-update.ts +++ b/server/lib/activitypub/send/send-update.ts @@ -61,7 +61,7 @@ async function sendUpdateActor (accountOrChannel: AccountModel | VideoChannelMod async function sendUpdateCacheFile (byActor: ActorModel, redundancyModel: VideoRedundancyModel) { logger.info('Creating job to update cache file %s.', redundancyModel.url) - const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(redundancyModel.VideoFile.Video.id) + const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(redundancyModel.getVideo().id) const activityBuilder = (audience: ActivityAudience) => { const redundancyObject = redundancyModel.toActivityPubObject() diff --git a/server/lib/activitypub/url.ts b/server/lib/activitypub/url.ts index 38f15448c..4229fe094 100644 --- a/server/lib/activitypub/url.ts +++ b/server/lib/activitypub/url.ts @@ -5,6 +5,8 @@ import { VideoModel } from '../../models/video/video' import { VideoAbuseModel } from '../../models/video/video-abuse' import { VideoCommentModel } from '../../models/video/video-comment' import { VideoFileModel } from '../../models/video/video-file' +import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model' +import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' function getVideoActivityPubUrl (video: VideoModel) { return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid @@ -16,6 +18,10 @@ function getVideoCacheFileActivityPubUrl (videoFile: VideoFileModel) { return `${CONFIG.WEBSERVER.URL}/redundancy/videos/${videoFile.Video.uuid}/${videoFile.resolution}${suffixFPS}` } +function getVideoCacheStreamingPlaylistActivityPubUrl (video: VideoModel, playlist: VideoStreamingPlaylistModel) { + return `${CONFIG.WEBSERVER.URL}/redundancy/video-playlists/${playlist.getStringType()}/${video.uuid}` +} + function getVideoCommentActivityPubUrl (video: VideoModel, videoComment: VideoCommentModel) { return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid + '/comments/' + videoComment.id } @@ -92,6 +98,7 @@ function getUndoActivityPubUrl (originalUrl: string) { export { getVideoActivityPubUrl, + getVideoCacheStreamingPlaylistActivityPubUrl, getVideoChannelActivityPubUrl, getAccountActivityPubUrl, getVideoAbuseActivityPubUrl, diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index e1e523499..edd01234f 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts @@ -2,7 +2,14 @@ import * as Bluebird from 'bluebird' import * as sequelize from 'sequelize' import * as magnetUtil from 'magnet-uri' import * as request from 'request' -import { ActivityIconObject, ActivityUrlObject, ActivityVideoUrlObject, VideoState } from '../../../shared/index' +import { + ActivityIconObject, + ActivityPlaylistSegmentHashesObject, + ActivityPlaylistUrlObject, + ActivityUrlObject, + ActivityVideoUrlObject, + VideoState +} from '../../../shared/index' import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' import { VideoPrivacy } from '../../../shared/models/videos' import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' @@ -30,6 +37,9 @@ import { AccountModel } from '../../models/account/account' import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' import { Notifier } from '../notifier' +import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' +import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' +import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model' async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { // If the video is not private and published, we federate it @@ -263,6 +273,25 @@ async function updateVideoFromAP (options: { options.video.VideoFiles = await Promise.all(upsertTasks) } + { + const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes(options.video, options.videoObject) + const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a)) + + // Remove video files that do not exist anymore + const destroyTasks = options.video.VideoStreamingPlaylists + .filter(f => !newStreamingPlaylists.find(newPlaylist => newPlaylist.hasSameUniqueKeysThan(f))) + .map(f => f.destroy(sequelizeOptions)) + await Promise.all(destroyTasks) + + // Update or add other one + const upsertTasks = streamingPlaylistAttributes.map(a => { + return VideoStreamingPlaylistModel.upsert(a, { returning: true, transaction: t }) + .then(([ streamingPlaylist ]) => streamingPlaylist) + }) + + options.video.VideoStreamingPlaylists = await Promise.all(upsertTasks) + } + { // Update Tags const tags = options.videoObject.tag.map(tag => tag.name) @@ -367,13 +396,25 @@ export { // --------------------------------------------------------------------------- -function isActivityVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject { +function isAPVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject { const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT) const urlMediaType = url.mediaType || url.mimeType 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' +} + +function isAPPlaylistSegmentHashesUrlObject (tag: any): tag is ActivityPlaylistSegmentHashesObject { + const urlMediaType = tag.mediaType || tag.mimeType + + return tag.name === 'sha256' && tag.type === 'Link' && urlMediaType === 'application/json' +} + async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) { logger.debug('Adding remote video %s.', videoObject.id) @@ -394,8 +435,14 @@ async function createVideo (videoObject: VideoTorrentObject, channelActor: Actor const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t })) await Promise.all(videoFilePromises) + const videoStreamingPlaylists = streamingPlaylistActivityUrlToDBAttributes(videoCreated, videoObject) + const playlistPromises = videoStreamingPlaylists.map(p => VideoStreamingPlaylistModel.create(p, { transaction: t })) + await Promise.all(playlistPromises) + // Process tags - const tags = videoObject.tag.map(t => t.name) + const tags = videoObject.tag + .filter(t => t.type === 'Hashtag') + .map(t => t.name) const tagInstances = await TagModel.findOrCreateTags(tags, t) await videoCreated.$set('Tags', tagInstances, sequelizeOptions) @@ -473,13 +520,13 @@ async function videoActivityObjectToDBAttributes ( } function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) { - const fileUrls = videoObject.url.filter(u => isActivityVideoUrlObject(u)) as ActivityVideoUrlObject[] + const fileUrls = videoObject.url.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[] if (fileUrls.length === 0) { throw new Error('Cannot find video files for ' + video.url) } - const attributes: VideoFileModel[] = [] + const attributes: FilteredModelAttributes[] = [] for (const fileUrl of fileUrls) { // Fetch associated magnet uri const magnet = videoObject.url.find(u => { @@ -502,7 +549,45 @@ function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: Vid size: fileUrl.size, videoId: video.id, fps: fileUrl.fps || -1 - } as VideoFileModel + } + + attributes.push(attribute) + } + + return attributes +} + +function streamingPlaylistActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) { + const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[] + if (playlistUrls.length === 0) return [] + + const attributes: FilteredModelAttributes[] = [] + for (const playlistUrlObject of playlistUrls) { + const p2pMediaLoaderInfohashes = playlistUrlObject.tag + .filter(t => t.type === 'Infohash') + .map(t => t.name) + if (p2pMediaLoaderInfohashes.length === 0) { + logger.warn('No infohashes found in AP playlist object.', { playlistUrl: playlistUrlObject }) + continue + } + + const segmentsSha256UrlObject = playlistUrlObject.tag + .find(t => { + return isAPPlaylistSegmentHashesUrlObject(t) + }) as ActivityPlaylistSegmentHashesObject + if (!segmentsSha256UrlObject) { + logger.warn('No segment sha256 URL found in AP playlist object.', { playlistUrl: playlistUrlObject }) + continue + } + + const attribute = { + type: VideoStreamingPlaylistType.HLS, + playlistUrl: playlistUrlObject.href, + segmentsSha256Url: segmentsSha256UrlObject.href, + p2pMediaLoaderInfohashes, + videoId: video.id + } + attributes.push(attribute) } -- cgit v1.2.3