"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.injectQueryToPlaylistUrls = exports.updatePlaylistAfterFileChange = exports.updateStreamingPlaylistsInfohashesIfNeeded = exports.downloadPlaylistSegments = exports.buildSha256Segment = exports.updateSha256VODSegments = exports.updateMasterHLSPlaylist = void 0;
const tslib_1 = require("tslib");
const fs_extra_1 = require("fs-extra");
const lodash_1 = require("lodash");
const p_queue_1 = tslib_1.__importDefault(require("p-queue"));
const path_1 = require("path");
const core_utils_1 = require("../../shared/core-utils");
const extra_utils_1 = require("../../shared/extra-utils");
const ffmpeg_1 = require("../helpers/ffmpeg");
const logger_1 = require("../helpers/logger");
const requests_1 = require("../helpers/requests");
const utils_1 = require("../helpers/utils");
const config_1 = require("../initializers/config");
const constants_1 = require("../initializers/constants");
const database_1 = require("../initializers/database");
const video_file_1 = require("../models/video/video-file");
const video_streaming_playlist_1 = require("../models/video/video-streaming-playlist");
const object_storage_1 = require("./object-storage");
const paths_1 = require("./paths");
const video_path_manager_1 = require("./video-path-manager");
function updateStreamingPlaylistsInfohashesIfNeeded() {
    return tslib_1.__awaiter(this, void 0, void 0, function* () {
        const playlistsToUpdate = yield video_streaming_playlist_1.VideoStreamingPlaylistModel.listByIncorrectPeerVersion();
        for (const playlist of playlistsToUpdate) {
            yield database_1.sequelizeTypescript.transaction((t) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                const videoFiles = yield video_file_1.VideoFileModel.listByStreamingPlaylist(playlist.id, t);
                playlist.assignP2PMediaLoaderInfoHashes(playlist.Video, videoFiles);
                playlist.p2pMediaLoaderPeerVersion = constants_1.P2P_MEDIA_LOADER_PEER_VERSION;
                yield playlist.save({ transaction: t });
            }));
        }
    });
}
exports.updateStreamingPlaylistsInfohashesIfNeeded = updateStreamingPlaylistsInfohashesIfNeeded;
function updatePlaylistAfterFileChange(video, playlist) {
    return tslib_1.__awaiter(this, void 0, void 0, function* () {
        try {
            let playlistWithFiles = yield updateMasterHLSPlaylist(video, playlist);
            playlistWithFiles = yield updateSha256VODSegments(video, playlist);
            playlistWithFiles = yield video_streaming_playlist_1.VideoStreamingPlaylistModel.loadWithVideoAndFiles(playlist.id);
            playlistWithFiles.assignP2PMediaLoaderInfoHashes(video, playlistWithFiles.VideoFiles);
            yield playlistWithFiles.save();
            video.setHLSPlaylist(playlistWithFiles);
        }
        catch (err) {
            logger_1.logger.info('Cannot update playlist after file change. Maybe due to concurrent transcoding', { err });
        }
    });
}
exports.updatePlaylistAfterFileChange = updatePlaylistAfterFileChange;
const playlistFilesQueue = new p_queue_1.default({ concurrency: 1 });
function updateMasterHLSPlaylist(video, playlistArg) {
    return playlistFilesQueue.add(() => tslib_1.__awaiter(this, void 0, void 0, function* () {
        const playlist = yield video_streaming_playlist_1.VideoStreamingPlaylistModel.loadWithVideoAndFiles(playlistArg.id);
        const masterPlaylists = ['#EXTM3U', '#EXT-X-VERSION:3'];
        for (const file of playlist.VideoFiles) {
            const playlistFilename = (0, paths_1.getHlsResolutionPlaylistFilename)(file.filename);
            yield video_path_manager_1.VideoPathManager.Instance.makeAvailableVideoFile(file.withVideoOrPlaylist(playlist), (videoFilePath) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                const size = yield (0, ffmpeg_1.getVideoStreamDimensionsInfo)(videoFilePath);
                const bandwidth = 'BANDWIDTH=' + video.getBandwidthBits(file);
                const resolution = `RESOLUTION=${(size === null || size === void 0 ? void 0 : size.width) || 0}x${(size === null || size === void 0 ? void 0 : size.height) || 0}`;
                let line = `#EXT-X-STREAM-INF:${bandwidth},${resolution}`;
                if (file.fps)
                    line += ',FRAME-RATE=' + file.fps;
                const codecs = yield Promise.all([
                    (0, ffmpeg_1.getVideoStreamCodec)(videoFilePath),
                    (0, ffmpeg_1.getAudioStreamCodec)(videoFilePath)
                ]);
                line += `,CODECS="${codecs.filter(c => !!c).join(',')}"`;
                masterPlaylists.push(line);
                masterPlaylists.push(playlistFilename);
            }));
        }
        if (playlist.playlistFilename) {
            yield video.removeStreamingPlaylistFile(playlist, playlist.playlistFilename);
        }
        playlist.playlistFilename = (0, paths_1.generateHLSMasterPlaylistFilename)(video.isLive);
        const masterPlaylistPath = video_path_manager_1.VideoPathManager.Instance.getFSHLSOutputPath(video, playlist.playlistFilename);
        yield (0, fs_extra_1.writeFile)(masterPlaylistPath, masterPlaylists.join('\n') + '\n');
        if (playlist.storage === 1) {
            playlist.playlistUrl = yield (0, object_storage_1.storeHLSFileFromFilename)(playlist, playlist.playlistFilename);
            yield (0, fs_extra_1.remove)(masterPlaylistPath);
        }
        return playlist.save();
    }));
}
exports.updateMasterHLSPlaylist = updateMasterHLSPlaylist;
function updateSha256VODSegments(video, playlistArg) {
    return playlistFilesQueue.add(() => tslib_1.__awaiter(this, void 0, void 0, function* () {
        const json = {};
        const playlist = yield video_streaming_playlist_1.VideoStreamingPlaylistModel.loadWithVideoAndFiles(playlistArg.id);
        for (const file of playlist.VideoFiles) {
            const rangeHashes = {};
            const fileWithPlaylist = file.withVideoOrPlaylist(playlist);
            yield video_path_manager_1.VideoPathManager.Instance.makeAvailableVideoFile(fileWithPlaylist, videoPath => {
                return video_path_manager_1.VideoPathManager.Instance.makeAvailableResolutionPlaylistFile(fileWithPlaylist, (resolutionPlaylistPath) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                    const playlistContent = yield (0, fs_extra_1.readFile)(resolutionPlaylistPath);
                    const ranges = getRangesFromPlaylist(playlistContent.toString());
                    const fd = yield (0, fs_extra_1.open)(videoPath, 'r');
                    for (const range of ranges) {
                        const buf = Buffer.alloc(range.length);
                        yield (0, fs_extra_1.read)(fd, buf, 0, range.length, range.offset);
                        rangeHashes[`${range.offset}-${range.offset + range.length - 1}`] = (0, extra_utils_1.sha256)(buf);
                    }
                    yield (0, fs_extra_1.close)(fd);
                    const videoFilename = file.filename;
                    json[videoFilename] = rangeHashes;
                }));
            });
        }
        if (playlist.segmentsSha256Filename) {
            yield video.removeStreamingPlaylistFile(playlist, playlist.segmentsSha256Filename);
        }
        playlist.segmentsSha256Filename = (0, paths_1.generateHlsSha256SegmentsFilename)(video.isLive);
        const outputPath = video_path_manager_1.VideoPathManager.Instance.getFSHLSOutputPath(video, playlist.segmentsSha256Filename);
        yield (0, fs_extra_1.outputJSON)(outputPath, json);
        if (playlist.storage === 1) {
            playlist.segmentsSha256Url = yield (0, object_storage_1.storeHLSFileFromFilename)(playlist, playlist.segmentsSha256Filename);
            yield (0, fs_extra_1.remove)(outputPath);
        }
        return playlist.save();
    }));
}
exports.updateSha256VODSegments = updateSha256VODSegments;
function buildSha256Segment(segmentPath) {
    return tslib_1.__awaiter(this, void 0, void 0, function* () {
        const buf = yield (0, fs_extra_1.readFile)(segmentPath);
        return (0, extra_utils_1.sha256)(buf);
    });
}
exports.buildSha256Segment = buildSha256Segment;
function downloadPlaylistSegments(playlistUrl, destinationDir, timeout, bodyKBLimit) {
    let timer;
    let remainingBodyKBLimit = bodyKBLimit;
    logger_1.logger.info('Importing HLS playlist %s', playlistUrl);
    return new Promise((res, rej) => tslib_1.__awaiter(this, void 0, void 0, function* () {
        const tmpDirectory = (0, path_1.join)(config_1.CONFIG.STORAGE.TMP_DIR, yield (0, utils_1.generateRandomString)(10));
        yield (0, fs_extra_1.ensureDir)(tmpDirectory);
        timer = setTimeout(() => {
            deleteTmpDirectory(tmpDirectory);
            return rej(new Error('HLS download timeout.'));
        }, timeout);
        try {
            const subPlaylistUrls = yield fetchUniqUrls(playlistUrl);
            const subRequests = subPlaylistUrls.map(u => fetchUniqUrls(u));
            const fileUrls = (0, core_utils_1.uniqify)((0, lodash_1.flatten)(yield Promise.all(subRequests)));
            logger_1.logger.debug('Will download %d HLS files.', fileUrls.length, { fileUrls });
            for (const fileUrl of fileUrls) {
                const destPath = (0, path_1.join)(tmpDirectory, (0, path_1.basename)(fileUrl));
                yield (0, requests_1.doRequestAndSaveToFile)(fileUrl, destPath, { bodyKBLimit: remainingBodyKBLimit, timeout: constants_1.REQUEST_TIMEOUTS.REDUNDANCY });
                const { size } = yield (0, fs_extra_1.stat)(destPath);
                remainingBodyKBLimit -= (size / 1000);
                logger_1.logger.debug('Downloaded HLS playlist file %s with %d kB remained limit.', fileUrl, Math.floor(remainingBodyKBLimit));
            }
            clearTimeout(timer);
            yield (0, fs_extra_1.move)(tmpDirectory, destinationDir, { overwrite: true });
            return res();
        }
        catch (err) {
            deleteTmpDirectory(tmpDirectory);
            return rej(err);
        }
    }));
    function deleteTmpDirectory(directory) {
        (0, fs_extra_1.remove)(directory)
            .catch(err => logger_1.logger.error('Cannot delete path on HLS download error.', { err }));
    }
    function fetchUniqUrls(playlistUrl) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const { body } = yield (0, requests_1.doRequest)(playlistUrl);
            if (!body)
                return [];
            const urls = body.split('\n')
                .filter(line => line.endsWith('.m3u8') || line.endsWith('.mp4'))
                .map(url => {
                if (url.startsWith('http://') || url.startsWith('https://'))
                    return url;
                return `${(0, path_1.dirname)(playlistUrl)}/${url}`;
            });
            return (0, core_utils_1.uniqify)(urls);
        });
    }
}
exports.downloadPlaylistSegments = downloadPlaylistSegments;
function injectQueryToPlaylistUrls(content, queryString) {
    return content.replace(/\.(m3u8|ts|mp4)/gm, '.$1?' + queryString);
}
exports.injectQueryToPlaylistUrls = injectQueryToPlaylistUrls;
function getRangesFromPlaylist(playlistContent) {
    const ranges = [];
    const lines = playlistContent.split('\n');
    const regex = /^#EXT-X-BYTERANGE:(\d+)@(\d+)$/;
    for (const line of lines) {
        const captured = regex.exec(line);
        if (captured) {
            ranges.push({ length: parseInt(captured[1], 10), offset: parseInt(captured[2], 10) });
        }
    }
    return ranges;
}
//# sourceMappingURL=hls.js.map