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/initializers/config.ts | 21 +++++++++ server/initializers/constants.ts | 50 +++++++++++++++------- server/initializers/database.ts | 10 +++-- server/initializers/migrations/0535-video-live.ts | 39 +++++++++++++++++ .../migrations/0540-video-file-infohash.ts | 26 +++++++++++ 5 files changed, 127 insertions(+), 19 deletions(-) create mode 100644 server/initializers/migrations/0535-video-live.ts create mode 100644 server/initializers/migrations/0540-video-file-infohash.ts (limited to 'server/initializers') diff --git a/server/initializers/config.ts b/server/initializers/config.ts index b40e525a5..7a8200ed9 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts @@ -198,6 +198,27 @@ const CONFIG = { get ENABLED () { return config.get('transcoding.webtorrent.enabled') } } }, + LIVE: { + get ENABLED () { return config.get('live.enabled') }, + + RTMP: { + get PORT () { return config.get('live.rtmp.port') } + }, + + TRANSCODING: { + get ENABLED () { return config.get('live.transcoding.enabled') }, + get THREADS () { return config.get('live.transcoding.threads') }, + + RESOLUTIONS: { + get '240p' () { return config.get('live.transcoding.resolutions.240p') }, + get '360p' () { return config.get('live.transcoding.resolutions.360p') }, + get '480p' () { return config.get('live.transcoding.resolutions.480p') }, + get '720p' () { return config.get('live.transcoding.resolutions.720p') }, + get '1080p' () { return config.get('live.transcoding.resolutions.1080p') }, + get '2160p' () { return config.get('live.transcoding.resolutions.2160p') } + } + } + }, IMPORT: { VIDEOS: { HTTP: { diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 171e9e9c2..606eeba2d 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -23,7 +23,7 @@ import { CONFIG, registerConfigChangedHandler } from './config' // --------------------------------------------------------------------------- -const LAST_MIGRATION_VERSION = 530 +const LAST_MIGRATION_VERSION = 540 // --------------------------------------------------------------------------- @@ -50,7 +50,8 @@ const WEBSERVER = { SCHEME: '', WS: '', HOSTNAME: '', - PORT: 0 + PORT: 0, + RTMP_URL: '' } // Sortable columns per schema @@ -264,7 +265,7 @@ const CONSTRAINTS_FIELDS = { VIEWS: { min: 0 }, LIKES: { min: 0 }, DISLIKES: { min: 0 }, - FILE_SIZE: { min: 10 }, + FILE_SIZE: { min: -1 }, URL: { min: 3, max: 2000 } // Length }, VIDEO_PLAYLISTS: { @@ -370,39 +371,41 @@ const VIDEO_LICENCES = { const VIDEO_LANGUAGES: { [id: string]: string } = {} -const VIDEO_PRIVACIES = { +const VIDEO_PRIVACIES: { [ id in VideoPrivacy ]: string } = { [VideoPrivacy.PUBLIC]: 'Public', [VideoPrivacy.UNLISTED]: 'Unlisted', [VideoPrivacy.PRIVATE]: 'Private', [VideoPrivacy.INTERNAL]: 'Internal' } -const VIDEO_STATES = { +const VIDEO_STATES: { [ id in VideoState ]: string } = { [VideoState.PUBLISHED]: 'Published', [VideoState.TO_TRANSCODE]: 'To transcode', - [VideoState.TO_IMPORT]: 'To import' + [VideoState.TO_IMPORT]: 'To import', + [VideoState.WAITING_FOR_LIVE]: 'Waiting for livestream', + [VideoState.LIVE_ENDED]: 'Livestream ended' } -const VIDEO_IMPORT_STATES = { +const VIDEO_IMPORT_STATES: { [ id in VideoImportState ]: string } = { [VideoImportState.FAILED]: 'Failed', [VideoImportState.PENDING]: 'Pending', [VideoImportState.SUCCESS]: 'Success', [VideoImportState.REJECTED]: 'Rejected' } -const ABUSE_STATES = { +const ABUSE_STATES: { [ id in AbuseState ]: string } = { [AbuseState.PENDING]: 'Pending', [AbuseState.REJECTED]: 'Rejected', [AbuseState.ACCEPTED]: 'Accepted' } -const VIDEO_PLAYLIST_PRIVACIES = { +const VIDEO_PLAYLIST_PRIVACIES: { [ id in VideoPlaylistPrivacy ]: string } = { [VideoPlaylistPrivacy.PUBLIC]: 'Public', [VideoPlaylistPrivacy.UNLISTED]: 'Unlisted', [VideoPlaylistPrivacy.PRIVATE]: 'Private' } -const VIDEO_PLAYLIST_TYPES = { +const VIDEO_PLAYLIST_TYPES: { [ id in VideoPlaylistType ]: string } = { [VideoPlaylistType.REGULAR]: 'Regular', [VideoPlaylistType.WATCH_LATER]: 'Watch later' } @@ -600,6 +603,17 @@ const LRU_CACHE = { const HLS_STREAMING_PLAYLIST_DIRECTORY = join(CONFIG.STORAGE.STREAMING_PLAYLISTS_DIR, 'hls') const HLS_REDUNDANCY_DIRECTORY = join(CONFIG.STORAGE.REDUNDANCY_DIR, 'hls') +const VIDEO_LIVE = { + EXTENSION: '.ts', + RTMP: { + CHUNK_SIZE: 60000, + GOP_CACHE: true, + PING: 60, + PING_TIMEOUT: 30, + BASE_PATH: 'live' + } +} + const MEMOIZE_TTL = { OVERVIEWS_SAMPLE: 1000 * 3600 * 4, // 4 hours INFO_HASH_EXISTS: 1000 * 3600 * 12 // 12 hours @@ -622,7 +636,8 @@ const REDUNDANCY = { const ACCEPT_HEADERS = [ 'html', 'application/json' ].concat(ACTIVITY_PUB.POTENTIAL_ACCEPT_HEADERS) const ASSETS_PATH = { - DEFAULT_AUDIO_BACKGROUND: join(root(), 'dist', 'server', 'assets', 'default-audio-background.jpg') + DEFAULT_AUDIO_BACKGROUND: join(root(), 'dist', 'server', 'assets', 'default-audio-background.jpg'), + DEFAULT_LIVE_BACKGROUND: join(root(), 'dist', 'server', 'assets', 'default-live-background.jpg') } // --------------------------------------------------------------------------- @@ -688,9 +703,9 @@ if (isTestInstance() === true) { STATIC_MAX_AGE.SERVER = '0' ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE = 2 - ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL = 10 * 1000 // 10 seconds - ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL = 10 * 1000 // 10 seconds - ACTIVITY_PUB.VIDEO_PLAYLIST_REFRESH_INTERVAL = 10 * 1000 // 10 seconds + ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL = 100 * 10000 // 10 seconds + ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL = 100 * 10000 // 10 seconds + ACTIVITY_PUB.VIDEO_PLAYLIST_REFRESH_INTERVAL = 100 * 10000 // 10 seconds CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max = 100 * 1024 // 100KB @@ -737,6 +752,7 @@ const FILES_CONTENT_HASH = { export { WEBSERVER, API_VERSION, + VIDEO_LIVE, PEERTUBE_VERSION, LAZY_STATIC_PATHS, SEARCH_INDEX, @@ -892,10 +908,14 @@ function buildVideoMimetypeExt () { function updateWebserverUrls () { WEBSERVER.URL = sanitizeUrl(CONFIG.WEBSERVER.SCHEME + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT) WEBSERVER.HOST = sanitizeHost(CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT, REMOTE_SCHEME.HTTP) - WEBSERVER.SCHEME = CONFIG.WEBSERVER.SCHEME WEBSERVER.WS = CONFIG.WEBSERVER.WS + + WEBSERVER.SCHEME = CONFIG.WEBSERVER.SCHEME WEBSERVER.HOSTNAME = CONFIG.WEBSERVER.HOSTNAME WEBSERVER.PORT = CONFIG.WEBSERVER.PORT + WEBSERVER.PORT = CONFIG.WEBSERVER.PORT + + WEBSERVER.RTMP_URL = 'rtmp://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.LIVE.RTMP.PORT + '/' + VIDEO_LIVE.RTMP.BASE_PATH } function updateWebserverConfig () { diff --git a/server/initializers/database.ts b/server/initializers/database.ts index a20cdacc3..128ed5b75 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts @@ -1,11 +1,11 @@ import { QueryTypes, Transaction } from 'sequelize' import { Sequelize as SequelizeTypescript } from 'sequelize-typescript' -import { AbuseModel } from '@server/models/abuse/abuse' -import { AbuseMessageModel } from '@server/models/abuse/abuse-message' -import { VideoAbuseModel } from '@server/models/abuse/video-abuse' -import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse' import { isTestInstance } from '../helpers/core-utils' import { logger } from '../helpers/logger' +import { AbuseModel } from '../models/abuse/abuse' +import { AbuseMessageModel } from '../models/abuse/abuse-message' +import { VideoAbuseModel } from '../models/abuse/video-abuse' +import { VideoCommentAbuseModel } from '../models/abuse/video-comment-abuse' import { AccountModel } from '../models/account/account' import { AccountBlocklistModel } from '../models/account/account-blocklist' import { AccountVideoRateModel } from '../models/account/account-video-rate' @@ -34,6 +34,7 @@ import { VideoChannelModel } from '../models/video/video-channel' import { VideoCommentModel } from '../models/video/video-comment' import { VideoFileModel } from '../models/video/video-file' import { VideoImportModel } from '../models/video/video-import' +import { VideoLiveModel } from '../models/video/video-live' import { VideoPlaylistModel } from '../models/video/video-playlist' import { VideoPlaylistElementModel } from '../models/video/video-playlist-element' import { VideoShareModel } from '../models/video/video-share' @@ -118,6 +119,7 @@ async function initDatabaseModels (silent: boolean) { VideoViewModel, VideoRedundancyModel, UserVideoHistoryModel, + VideoLiveModel, AccountBlocklistModel, ServerBlocklistModel, UserNotificationModel, diff --git a/server/initializers/migrations/0535-video-live.ts b/server/initializers/migrations/0535-video-live.ts new file mode 100644 index 000000000..35523efc4 --- /dev/null +++ b/server/initializers/migrations/0535-video-live.ts @@ -0,0 +1,39 @@ +import * as Sequelize from 'sequelize' + +async function up (utils: { + transaction: Sequelize.Transaction + queryInterface: Sequelize.QueryInterface + sequelize: Sequelize.Sequelize +}): Promise { + { + const query = ` + CREATE TABLE IF NOT EXISTS "videoLive" ( + "id" SERIAL , + "streamKey" VARCHAR(255) NOT NULL, + "videoId" INTEGER NOT NULL REFERENCES "video" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, + "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, + PRIMARY KEY ("id") + ); + ` + + await utils.sequelize.query(query) + } + + { + await utils.queryInterface.addColumn('video', 'isLive', { + type: Sequelize.BOOLEAN, + defaultValue: false, + allowNull: false + }) + } +} + +function down (options) { + throw new Error('Not implemented.') +} + +export { + up, + down +} diff --git a/server/initializers/migrations/0540-video-file-infohash.ts b/server/initializers/migrations/0540-video-file-infohash.ts new file mode 100644 index 000000000..550178dab --- /dev/null +++ b/server/initializers/migrations/0540-video-file-infohash.ts @@ -0,0 +1,26 @@ +import * as Sequelize from 'sequelize' + +async function up (utils: { + transaction: Sequelize.Transaction + queryInterface: Sequelize.QueryInterface + sequelize: Sequelize.Sequelize +}): Promise { + { + const data = { + type: Sequelize.STRING, + defaultValue: null, + allowNull: true + } + + await utils.queryInterface.changeColumn('videoFile', 'infoHash', data) + } +} + +function down (options) { + throw new Error('Not implemented.') +} + +export { + up, + down +} -- cgit v1.2.3