"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LiveManager = void 0;
const tslib_1 = require("tslib");
const fs_extra_1 = require("fs-extra");
const net_1 = require("net");
const path_1 = require("path");
const tls_1 = require("tls");
const ffmpeg_1 = require("../../helpers/ffmpeg");
const logger_1 = require("../../helpers/logger");
const config_1 = require("../../initializers/config");
const constants_1 = require("../../initializers/constants");
const user_1 = require("../../models/user/user");
const video_1 = require("../../models/video/video");
const video_live_1 = require("../../models/video/video-live");
const video_live_session_1 = require("../../models/video/video-live-session");
const video_streaming_playlist_1 = require("../../models/video/video-streaming-playlist");
const core_utils_1 = require("../../../shared/core-utils");
const videos_1 = require("../activitypub/videos");
const job_queue_1 = require("../job-queue");
const paths_1 = require("../paths");
const peertube_socket_1 = require("../peertube-socket");
const hooks_1 = require("../plugins/hooks");
const live_quota_store_1 = require("./live-quota-store");
const live_utils_1 = require("./live-utils");
const shared_1 = require("./shared");
const NodeRtmpSession = require('node-media-server/src/node_rtmp_session');
const context = require('node-media-server/src/node_core_ctx');
const nodeMediaServerLogger = require('node-media-server/src/node_core_logger');
nodeMediaServerLogger.setLogType(0);
const config = {
    rtmp: {
        port: config_1.CONFIG.LIVE.RTMP.PORT,
        chunk_size: constants_1.VIDEO_LIVE.RTMP.CHUNK_SIZE,
        gop_cache: constants_1.VIDEO_LIVE.RTMP.GOP_CACHE,
        ping: constants_1.VIDEO_LIVE.RTMP.PING,
        ping_timeout: constants_1.VIDEO_LIVE.RTMP.PING_TIMEOUT
    }
};
const lTags = (0, logger_1.loggerTagsFactory)('live');
class LiveManager {
    constructor() {
        this.muxingSessions = new Map();
        this.videoSessions = new Map();
        this.running = false;
    }
    init() {
        const events = this.getContext().nodeEvent;
        events.on('postPublish', (sessionId, streamPath) => {
            logger_1.logger.debug('RTMP received stream', Object.assign({ id: sessionId, streamPath }, lTags(sessionId)));
            const splittedPath = streamPath.split('/');
            if (splittedPath.length !== 3 || splittedPath[1] !== constants_1.VIDEO_LIVE.RTMP.BASE_PATH) {
                logger_1.logger.warn('Live path is incorrect.', Object.assign({ streamPath }, lTags(sessionId)));
                return this.abortSession(sessionId);
            }
            const session = this.getContext().sessions.get(sessionId);
            this.handleSession(sessionId, session.inputOriginUrl + streamPath, splittedPath[2])
                .catch(err => logger_1.logger.error('Cannot handle sessions.', Object.assign({ err }, lTags(sessionId))));
        });
        events.on('donePublish', sessionId => {
            logger_1.logger.info('Live session ended.', Object.assign({ sessionId }, lTags(sessionId)));
        });
        (0, config_1.registerConfigChangedHandler)(() => {
            if (!this.running && config_1.CONFIG.LIVE.ENABLED === true) {
                this.run().catch(err => logger_1.logger.error('Cannot run live server.', { err }));
                return;
            }
            if (this.running && config_1.CONFIG.LIVE.ENABLED === false) {
                this.stop();
            }
        });
        this.handleBrokenLives()
            .catch(err => logger_1.logger.error('Cannot handle broken lives.', Object.assign({ err }, lTags())));
    }
    run() {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            this.running = true;
            if (config_1.CONFIG.LIVE.RTMP.ENABLED) {
                logger_1.logger.info('Running RTMP server on port %d', config_1.CONFIG.LIVE.RTMP.PORT, lTags());
                this.rtmpServer = (0, net_1.createServer)(socket => {
                    const session = new NodeRtmpSession(config, socket);
                    session.inputOriginUrl = 'rtmp://127.0.0.1:' + config_1.CONFIG.LIVE.RTMP.PORT;
                    session.run();
                });
                this.rtmpServer.on('error', err => {
                    logger_1.logger.error('Cannot run RTMP server.', Object.assign({ err }, lTags()));
                });
                this.rtmpServer.listen(config_1.CONFIG.LIVE.RTMP.PORT, config_1.CONFIG.LIVE.RTMP.HOSTNAME);
            }
            if (config_1.CONFIG.LIVE.RTMPS.ENABLED) {
                logger_1.logger.info('Running RTMPS server on port %d', config_1.CONFIG.LIVE.RTMPS.PORT, lTags());
                const [key, cert] = yield Promise.all([
                    (0, fs_extra_1.readFile)(config_1.CONFIG.LIVE.RTMPS.KEY_FILE),
                    (0, fs_extra_1.readFile)(config_1.CONFIG.LIVE.RTMPS.CERT_FILE)
                ]);
                const serverOptions = { key, cert };
                this.rtmpsServer = (0, tls_1.createServer)(serverOptions, socket => {
                    const session = new NodeRtmpSession(config, socket);
                    session.inputOriginUrl = 'rtmps://127.0.0.1:' + config_1.CONFIG.LIVE.RTMPS.PORT;
                    session.run();
                });
                this.rtmpsServer.on('error', err => {
                    logger_1.logger.error('Cannot run RTMPS server.', Object.assign({ err }, lTags()));
                });
                this.rtmpsServer.listen(config_1.CONFIG.LIVE.RTMPS.PORT, config_1.CONFIG.LIVE.RTMPS.HOSTNAME);
            }
        });
    }
    stop() {
        this.running = false;
        if (this.rtmpServer) {
            logger_1.logger.info('Stopping RTMP server.', lTags());
            this.rtmpServer.close();
            this.rtmpServer = undefined;
        }
        if (this.rtmpsServer) {
            logger_1.logger.info('Stopping RTMPS server.', lTags());
            this.rtmpsServer.close();
            this.rtmpsServer = undefined;
        }
        this.getContext().sessions.forEach((session) => {
            if (session instanceof NodeRtmpSession) {
                session.stop();
            }
        });
    }
    isRunning() {
        return !!this.rtmpServer;
    }
    stopSessionOf(videoId, error) {
        const sessionId = this.videoSessions.get(videoId);
        if (!sessionId)
            return;
        this.saveEndingSession(videoId, error)
            .catch(err => logger_1.logger.error('Cannot save ending session.', Object.assign({ err }, lTags(sessionId))));
        this.videoSessions.delete(videoId);
        this.abortSession(sessionId);
    }
    getContext() {
        return context;
    }
    abortSession(sessionId) {
        const session = this.getContext().sessions.get(sessionId);
        if (session) {
            session.stop();
            this.getContext().sessions.delete(sessionId);
        }
        const muxingSession = this.muxingSessions.get(sessionId);
        if (muxingSession) {
            muxingSession.abort();
            this.muxingSessions.delete(sessionId);
        }
    }
    handleSession(sessionId, inputUrl, streamKey) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const videoLive = yield video_live_1.VideoLiveModel.loadByStreamKey(streamKey);
            if (!videoLive) {
                logger_1.logger.warn('Unknown live video with stream key %s.', streamKey, lTags(sessionId));
                return this.abortSession(sessionId);
            }
            const video = videoLive.Video;
            if (video.isBlacklisted()) {
                logger_1.logger.warn('Video is blacklisted. Refusing stream %s.', streamKey, lTags(sessionId, video.uuid));
                return this.abortSession(sessionId);
            }
            const oldStreamingPlaylist = yield video_streaming_playlist_1.VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id);
            if (oldStreamingPlaylist) {
                if (!videoLive.permanentLive)
                    throw new Error('Found previous session in a non permanent live: ' + video.uuid);
                yield (0, live_utils_1.cleanupAndDestroyPermanentLive)(video, oldStreamingPlaylist);
            }
            this.videoSessions.set(video.id, sessionId);
            const now = Date.now();
            const probe = yield (0, ffmpeg_1.ffprobePromise)(inputUrl);
            const [{ resolution, ratio }, fps, bitrate, hasAudio] = yield Promise.all([
                (0, ffmpeg_1.getVideoStreamDimensionsInfo)(inputUrl, probe),
                (0, ffmpeg_1.getVideoStreamFPS)(inputUrl, probe),
                (0, ffmpeg_1.getVideoStreamBitrate)(inputUrl, probe),
                (0, ffmpeg_1.hasAudioStream)(inputUrl, probe)
            ]);
            logger_1.logger.info('%s probing took %d ms (bitrate: %d, fps: %d, resolution: %d)', inputUrl, Date.now() - now, bitrate, fps, resolution, lTags(sessionId, video.uuid));
            const allResolutions = yield hooks_1.Hooks.wrapObject(this.buildAllResolutionsToTranscode(resolution, hasAudio), 'filter:transcoding.auto.resolutions-to-transcode.result', { video });
            logger_1.logger.info('Will mux/transcode live video of original resolution %d.', resolution, Object.assign({ allResolutions }, lTags(sessionId, video.uuid)));
            const streamingPlaylist = yield this.createLivePlaylist(video, allResolutions);
            return this.runMuxingSession({
                sessionId,
                videoLive,
                streamingPlaylist,
                inputUrl,
                fps,
                bitrate,
                ratio,
                allResolutions,
                hasAudio
            });
        });
    }
    runMuxingSession(options) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const { sessionId, videoLive } = options;
            const videoUUID = videoLive.Video.uuid;
            const localLTags = lTags(sessionId, videoUUID);
            const liveSession = yield this.saveStartingSession(videoLive);
            const user = yield user_1.UserModel.loadByLiveId(videoLive.id);
            live_quota_store_1.LiveQuotaStore.Instance.addNewLive(user.id, videoLive.id);
            const muxingSession = new shared_1.MuxingSession(Object.assign({ context: this.getContext(), sessionId,
                videoLive,
                user }, (0, core_utils_1.pick)(options, ['streamingPlaylist', 'inputUrl', 'bitrate', 'ratio', 'fps', 'allResolutions', 'hasAudio'])));
            muxingSession.on('live-ready', () => this.publishAndFederateLive(videoLive, localLTags));
            muxingSession.on('bad-socket-health', ({ videoId }) => {
                logger_1.logger.error('Too much data in client socket stream (ffmpeg is too slow to transcode the video).' +
                    ' Stopping session of video %s.', videoUUID, localLTags);
                this.stopSessionOf(videoId, 1);
            });
            muxingSession.on('duration-exceeded', ({ videoId }) => {
                logger_1.logger.info('Stopping session of %s: max duration exceeded.', videoUUID, localLTags);
                this.stopSessionOf(videoId, 2);
            });
            muxingSession.on('quota-exceeded', ({ videoId }) => {
                logger_1.logger.info('Stopping session of %s: user quota exceeded.', videoUUID, localLTags);
                this.stopSessionOf(videoId, 3);
            });
            muxingSession.on('ffmpeg-error', ({ videoId }) => {
                this.stopSessionOf(videoId, 4);
            });
            muxingSession.on('ffmpeg-end', ({ videoId }) => {
                this.onMuxingFFmpegEnd(videoId, sessionId);
            });
            muxingSession.on('after-cleanup', ({ videoId }) => {
                this.muxingSessions.delete(sessionId);
                live_quota_store_1.LiveQuotaStore.Instance.removeLive(user.id, videoLive.id);
                muxingSession.destroy();
                return this.onAfterMuxingCleanup({ videoId, liveSession })
                    .catch(err => logger_1.logger.error('Error in end transmuxing.', Object.assign({ err }, localLTags)));
            });
            this.muxingSessions.set(sessionId, muxingSession);
            muxingSession.runMuxing()
                .catch(err => {
                logger_1.logger.error('Cannot run muxing.', Object.assign({ err }, localLTags));
                this.abortSession(sessionId);
            });
        });
    }
    publishAndFederateLive(live, localLTags) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const videoId = live.videoId;
            try {
                const video = yield video_1.VideoModel.loadFull(videoId);
                logger_1.logger.info('Will publish and federate live %s.', video.url, localLTags);
                video.state = 1;
                video.publishedAt = new Date();
                yield video.save();
                live.Video = video;
                yield (0, core_utils_1.wait)((0, ffmpeg_1.getLiveSegmentTime)(live.latencyMode) * 1000 * constants_1.VIDEO_LIVE.EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION);
                try {
                    yield (0, videos_1.federateVideoIfNeeded)(video, false);
                }
                catch (err) {
                    logger_1.logger.error('Cannot federate live video %s.', video.url, Object.assign({ err }, localLTags));
                }
                peertube_socket_1.PeerTubeSocket.Instance.sendVideoLiveNewState(video);
            }
            catch (err) {
                logger_1.logger.error('Cannot save/federate live video %d.', videoId, Object.assign({ err }, localLTags));
            }
        });
    }
    onMuxingFFmpegEnd(videoId, sessionId) {
        this.videoSessions.delete(videoId);
        this.saveEndingSession(videoId, null)
            .catch(err => logger_1.logger.error('Cannot save ending session.', Object.assign({ err }, lTags(sessionId))));
    }
    onAfterMuxingCleanup(options) {
        var _a;
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const { videoId, liveSession: liveSessionArg, cleanupNow = false } = options;
            try {
                const fullVideo = yield video_1.VideoModel.loadFull(videoId);
                if (!fullVideo)
                    return;
                const live = yield video_live_1.VideoLiveModel.loadByVideoId(fullVideo.id);
                const liveSession = liveSessionArg !== null && liveSessionArg !== void 0 ? liveSessionArg : yield video_live_session_1.VideoLiveSessionModel.findLatestSessionOf(fullVideo.id);
                if (!liveSession.endDate) {
                    liveSession.endDate = new Date();
                    yield liveSession.save();
                }
                job_queue_1.JobQueue.Instance.createJobAsync({
                    type: 'video-live-ending',
                    payload: {
                        videoId: fullVideo.id,
                        replayDirectory: live.saveReplay
                            ? yield this.findReplayDirectory(fullVideo)
                            : undefined,
                        liveSessionId: liveSession.id,
                        streamingPlaylistId: (_a = fullVideo.getHLSPlaylist()) === null || _a === void 0 ? void 0 : _a.id,
                        publishedAt: fullVideo.publishedAt.toISOString()
                    },
                    delay: cleanupNow
                        ? 0
                        : constants_1.VIDEO_LIVE.CLEANUP_DELAY
                });
                fullVideo.state = live.permanentLive
                    ? 4
                    : 5;
                yield fullVideo.save();
                peertube_socket_1.PeerTubeSocket.Instance.sendVideoLiveNewState(fullVideo);
                yield (0, videos_1.federateVideoIfNeeded)(fullVideo, false);
            }
            catch (err) {
                logger_1.logger.error('Cannot save/federate new video state of live streaming of video %d.', videoId, Object.assign({ err }, lTags(videoId + '')));
            }
        });
    }
    handleBrokenLives() {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const videoUUIDs = yield video_1.VideoModel.listPublishedLiveUUIDs();
            for (const uuid of videoUUIDs) {
                yield this.onAfterMuxingCleanup({ videoId: uuid, cleanupNow: true });
            }
        });
    }
    findReplayDirectory(video) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const directory = (0, paths_1.getLiveReplayBaseDirectory)(video);
            const files = yield (0, fs_extra_1.readdir)(directory);
            if (files.length === 0)
                return undefined;
            return (0, path_1.join)(directory, files.sort().reverse()[0]);
        });
    }
    buildAllResolutionsToTranscode(originResolution, hasAudio) {
        const includeInput = config_1.CONFIG.LIVE.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION;
        const resolutionsEnabled = config_1.CONFIG.LIVE.TRANSCODING.ENABLED
            ? (0, ffmpeg_1.computeResolutionsToTranscode)({ input: originResolution, type: 'live', includeInput, strictLower: false, hasAudio })
            : [];
        if (resolutionsEnabled.length === 0) {
            return [originResolution];
        }
        return resolutionsEnabled;
    }
    createLivePlaylist(video, allResolutions) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const playlist = yield video_streaming_playlist_1.VideoStreamingPlaylistModel.loadOrGenerate(video);
            playlist.playlistFilename = (0, paths_1.generateHLSMasterPlaylistFilename)(true);
            playlist.segmentsSha256Filename = (0, paths_1.generateHlsSha256SegmentsFilename)(true);
            playlist.p2pMediaLoaderPeerVersion = constants_1.P2P_MEDIA_LOADER_PEER_VERSION;
            playlist.type = 1;
            playlist.storage = config_1.CONFIG.OBJECT_STORAGE.ENABLED
                ? 1
                : 0;
            playlist.assignP2PMediaLoaderInfoHashes(video, allResolutions);
            return playlist.save();
        });
    }
    saveStartingSession(videoLive) {
        const liveSession = new video_live_session_1.VideoLiveSessionModel({
            startDate: new Date(),
            liveVideoId: videoLive.videoId,
            saveReplay: videoLive.saveReplay,
            endingProcessed: false
        });
        return liveSession.save();
    }
    saveEndingSession(videoId, error) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const liveSession = yield video_live_session_1.VideoLiveSessionModel.findCurrentSessionOf(videoId);
            if (!liveSession)
                return;
            liveSession.endDate = new Date();
            liveSession.error = error;
            return liveSession.save();
        });
    }
    static get Instance() {
        return this.instance || (this.instance = new this());
    }
}
exports.LiveManager = LiveManager;
//# sourceMappingURL=live-manager.js.map