From c6c0fa6cd8fe8f752463d8982c3dbcd448739c4e Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 17 Sep 2020 09:20:52 +0200 Subject: Live streaming implementation first step --- server/controllers/api/config.ts | 39 +++++++++-- server/controllers/api/videos/index.ts | 4 +- server/controllers/api/videos/live.ts | 116 +++++++++++++++++++++++++++++++++ server/controllers/index.ts | 1 + server/controllers/live.ts | 29 +++++++++ server/controllers/static.ts | 9 ++- 6 files changed, 190 insertions(+), 8 deletions(-) create mode 100644 server/controllers/api/videos/live.ts create mode 100644 server/controllers/live.ts (limited to 'server/controllers') diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index b80ea4902..bd100ef9c 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts @@ -113,7 +113,15 @@ async function getConfig (req: express.Request, res: express.Response) { webtorrent: { enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED }, - enabledResolutions: getEnabledResolutions() + enabledResolutions: getEnabledResolutions('vod') + }, + live: { + enabled: CONFIG.LIVE.ENABLED, + + transcoding: { + enabled: CONFIG.LIVE.TRANSCODING.ENABLED, + enabledResolutions: getEnabledResolutions('live') + } }, import: { videos: { @@ -232,7 +240,7 @@ async function deleteCustomConfig (req: express.Request, res: express.Response) const data = customConfig() - return res.json(data).end() + return res.json(data) } async function updateCustomConfig (req: express.Request, res: express.Response) { @@ -254,7 +262,7 @@ async function updateCustomConfig (req: express.Request, res: express.Response) oldCustomConfigAuditKeys ) - return res.json(data).end() + return res.json(data) } function getRegisteredThemes () { @@ -268,9 +276,13 @@ function getRegisteredThemes () { })) } -function getEnabledResolutions () { - return Object.keys(CONFIG.TRANSCODING.RESOLUTIONS) - .filter(key => CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.RESOLUTIONS[key] === true) +function getEnabledResolutions (type: 'vod' | 'live') { + const transcoding = type === 'vod' + ? CONFIG.TRANSCODING + : CONFIG.LIVE.TRANSCODING + + return Object.keys(transcoding.RESOLUTIONS) + .filter(key => transcoding.ENABLED && transcoding.RESOLUTIONS[key] === true) .map(r => parseInt(r, 10)) } @@ -411,6 +423,21 @@ function customConfig (): CustomConfig { enabled: CONFIG.TRANSCODING.HLS.ENABLED } }, + live: { + enabled: CONFIG.LIVE.ENABLED, + transcoding: { + enabled: CONFIG.LIVE.TRANSCODING.ENABLED, + threads: CONFIG.LIVE.TRANSCODING.THREADS, + resolutions: { + '240p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['240p'], + '360p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['360p'], + '480p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['480p'], + '720p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['720p'], + '1080p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['1080p'], + '2160p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['2160p'] + } + } + }, import: { videos: { http: { diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 15b6f214f..94f0361ee 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts @@ -63,6 +63,7 @@ import { blacklistRouter } from './blacklist' import { videoCaptionsRouter } from './captions' import { videoCommentRouter } from './comment' import { videoImportsRouter } from './import' +import { liveRouter } from './live' import { ownershipVideoRouter } from './ownership' import { rateVideoRouter } from './rate' import { watchingRouter } from './watching' @@ -96,6 +97,7 @@ videosRouter.use('/', videoCaptionsRouter) videosRouter.use('/', videoImportsRouter) videosRouter.use('/', ownershipVideoRouter) videosRouter.use('/', watchingRouter) +videosRouter.use('/', liveRouter) videosRouter.get('/categories', listVideoCategories) videosRouter.get('/licences', listVideoLicences) @@ -304,7 +306,7 @@ async function addVideo (req: express.Request, res: express.Response) { id: videoCreated.id, uuid: videoCreated.uuid } - }).end() + }) } async function updateVideo (req: express.Request, res: express.Response) { diff --git a/server/controllers/api/videos/live.ts b/server/controllers/api/videos/live.ts new file mode 100644 index 000000000..d08ef9869 --- /dev/null +++ b/server/controllers/api/videos/live.ts @@ -0,0 +1,116 @@ +import * as express from 'express' +import { v4 as uuidv4 } from 'uuid' +import { createReqFiles } from '@server/helpers/express-utils' +import { CONFIG } from '@server/initializers/config' +import { ASSETS_PATH, MIMETYPES } from '@server/initializers/constants' +import { getVideoActivityPubUrl } from '@server/lib/activitypub/url' +import { videoLiveAddValidator, videoLiveGetValidator } from '@server/middlewares/validators/videos/video-live' +import { VideoLiveModel } from '@server/models/video/video-live' +import { MVideoDetails, MVideoFullLight } from '@server/types/models' +import { VideoCreate, VideoPrivacy, VideoState } from '../../../../shared' +import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' +import { logger } from '../../../helpers/logger' +import { sequelizeTypescript } from '../../../initializers/database' +import { createVideoMiniatureFromExisting } from '../../../lib/thumbnail' +import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares' +import { TagModel } from '../../../models/video/tag' +import { VideoModel } from '../../../models/video/video' +import { buildLocalVideoFromCreate } from '@server/lib/video' + +const liveRouter = express.Router() + +const reqVideoFileLive = createReqFiles( + [ 'thumbnailfile', 'previewfile' ], + MIMETYPES.IMAGE.MIMETYPE_EXT, + { + thumbnailfile: CONFIG.STORAGE.TMP_DIR, + previewfile: CONFIG.STORAGE.TMP_DIR + } +) + +liveRouter.post('/live', + authenticate, + reqVideoFileLive, + asyncMiddleware(videoLiveAddValidator), + asyncRetryTransactionMiddleware(addLiveVideo) +) + +liveRouter.get('/live/:videoId', + authenticate, + asyncMiddleware(videoLiveGetValidator), + asyncRetryTransactionMiddleware(getVideoLive) +) + +// --------------------------------------------------------------------------- + +export { + liveRouter +} + +// --------------------------------------------------------------------------- + +async function getVideoLive (req: express.Request, res: express.Response) { + const videoLive = res.locals.videoLive + + return res.json(videoLive.toFormattedJSON()) +} + +async function addLiveVideo (req: express.Request, res: express.Response) { + const videoInfo: VideoCreate = req.body + + // Prepare data so we don't block the transaction + const videoData = buildLocalVideoFromCreate(videoInfo, res.locals.videoChannel.id) + videoData.isLive = true + + const videoLive = new VideoLiveModel() + videoLive.streamKey = uuidv4() + + const video = new VideoModel(videoData) as MVideoDetails + video.url = getVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object + + // Process thumbnail or create it from the video + const thumbnailField = req.files ? req.files['thumbnailfile'] : null + const thumbnailModel = thumbnailField + ? await createVideoMiniatureFromExisting(thumbnailField[0].path, video, ThumbnailType.MINIATURE, false) + : await createVideoMiniatureFromExisting(ASSETS_PATH.DEFAULT_LIVE_BACKGROUND, video, ThumbnailType.MINIATURE, true) + + // Process preview or create it from the video + const previewField = req.files ? req.files['previewfile'] : null + const previewModel = previewField + ? await createVideoMiniatureFromExisting(previewField[0].path, video, ThumbnailType.PREVIEW, false) + : await createVideoMiniatureFromExisting(ASSETS_PATH.DEFAULT_LIVE_BACKGROUND, video, ThumbnailType.PREVIEW, true) + + const { videoCreated } = await sequelizeTypescript.transaction(async t => { + const sequelizeOptions = { transaction: t } + + const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight + + if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) + if (previewModel) await videoCreated.addAndSaveThumbnail(previewModel, t) + + // Do not forget to add video channel information to the created video + videoCreated.VideoChannel = res.locals.videoChannel + + videoLive.videoId = videoCreated.id + await videoLive.save(sequelizeOptions) + + // Create tags + if (videoInfo.tags !== undefined) { + const tagInstances = await TagModel.findOrCreateTags(videoInfo.tags, t) + + await video.$set('Tags', tagInstances, sequelizeOptions) + video.Tags = tagInstances + } + + logger.info('Video live %s with uuid %s created.', videoInfo.name, videoCreated.uuid) + + return { videoCreated } + }) + + return res.json({ + video: { + id: videoCreated.id, + uuid: videoCreated.uuid + } + }) +} diff --git a/server/controllers/index.ts b/server/controllers/index.ts index 0d64b33bb..5a199ae9c 100644 --- a/server/controllers/index.ts +++ b/server/controllers/index.ts @@ -5,6 +5,7 @@ export * from './feeds' export * from './services' export * from './static' export * from './lazy-static' +export * from './live' export * from './webfinger' export * from './tracker' export * from './bots' diff --git a/server/controllers/live.ts b/server/controllers/live.ts new file mode 100644 index 000000000..fa4c2cc1a --- /dev/null +++ b/server/controllers/live.ts @@ -0,0 +1,29 @@ +import * as express from 'express' +import { mapToJSON } from '@server/helpers/core-utils' +import { LiveManager } from '@server/lib/live-manager' + +const liveRouter = express.Router() + +liveRouter.use('/segments-sha256/:videoUUID', + getSegmentsSha256 +) + +// --------------------------------------------------------------------------- + +export { + liveRouter +} + +// --------------------------------------------------------------------------- + +function getSegmentsSha256 (req: express.Request, res: express.Response) { + const videoUUID = req.params.videoUUID + + const result = LiveManager.Instance.getSegmentsSha256(videoUUID) + + if (!result) { + return res.sendStatus(404) + } + + return res.json(mapToJSON(result)) +} diff --git a/server/controllers/static.ts b/server/controllers/static.ts index 3f7bbdbae..e04c27b11 100644 --- a/server/controllers/static.ts +++ b/server/controllers/static.ts @@ -260,7 +260,14 @@ async function generateNodeinfo (req: express.Request, res: express.Response) { webtorrent: { enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED }, - enabledResolutions: getEnabledResolutions() + enabledResolutions: getEnabledResolutions('vod') + }, + live: { + enabled: CONFIG.LIVE.ENABLED, + transcoding: { + enabled: CONFIG.LIVE.TRANSCODING.ENABLED, + enabledResolutions: getEnabledResolutions('live') + } }, import: { videos: { -- cgit v1.2.3