UNPKG

@eleven-am/transcoder

Version:

High-performance HLS transcoding library with hardware acceleration, intelligent client management, and distributed processing support for Node.js

362 lines 22.9 kB
"use strict"; /* * @eleven-am/transcoder * Copyright (C) 2025 Roy OSSAI * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; }; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var _HLSController_instances, _HLSController_hwConfig, _HLSController_streamMetricsEventHandler, _HLSController_segmentProcessor, _HLSController_hwDetector, _HLSController_metadataService, _HLSController_qualityService, _HLSController_streams, _HLSController_clientTracker, _HLSController_maxSegmentBatchSize, _HLSController_fileStorage, _HLSController_hwAccelEnabled, _HLSController_jobProcessor, _HLSController_distributedConfig, _HLSController_loadAdjustmentInterval, _HLSController_hookUpJobProcessor, _HLSController_hookUpClientTracker, _HLSController_hookUpStream, _HLSController_getOrCreateStream, _HLSController_getStreamAndPriority, _HLSController_buildMediaSource, _HLSController_prepareSegments; Object.defineProperty(exports, "__esModule", { value: true }); exports.HLSController = void 0; const fp_1 = require("@eleven-am/fp"); const clientTracker_1 = require("./clientTracker"); const distributed_1 = require("./distributed"); const fileDatabase_1 = require("./fileDatabase"); const fileStorage_1 = require("./fileStorage"); const hardwareAccelerationDetector_1 = require("./hardwareAccelerationDetector"); const jobProcessor_1 = require("./jobProcessor"); const mediaSource_1 = require("./mediaSource"); const metadataService_1 = require("./metadataService"); const qualityService_1 = require("./qualityService"); const stream_1 = require("./stream"); const types_1 = require("./types"); const utils_1 = require("./utils"); class HLSController { constructor(options) { _HLSController_instances.add(this); _HLSController_hwConfig.set(this, null); _HLSController_streamMetricsEventHandler.set(this, null); _HLSController_segmentProcessor.set(this, null); _HLSController_hwDetector.set(this, void 0); _HLSController_metadataService.set(this, void 0); _HLSController_qualityService.set(this, void 0); _HLSController_streams.set(this, void 0); _HLSController_clientTracker.set(this, void 0); _HLSController_maxSegmentBatchSize.set(this, void 0); _HLSController_fileStorage.set(this, void 0); _HLSController_hwAccelEnabled.set(this, void 0); _HLSController_jobProcessor.set(this, void 0); _HLSController_distributedConfig.set(this, void 0); _HLSController_loadAdjustmentInterval.set(this, null); __classPrivateFieldSet(this, _HLSController_streams, new Map(), "f"); __classPrivateFieldSet(this, _HLSController_hwAccelEnabled, options.hwAccel || false, "f"); __classPrivateFieldSet(this, _HLSController_maxSegmentBatchSize, options.maxSegmentBatchSize || 100, "f"); this.streamConfig = options.config || {}; __classPrivateFieldSet(this, _HLSController_distributedConfig, options.distributed, "f"); __classPrivateFieldSet(this, _HLSController_hwDetector, new hardwareAccelerationDetector_1.HardwareAccelerationDetector(), "f"); __classPrivateFieldSet(this, _HLSController_fileStorage, new fileStorage_1.FileStorage(options.cacheDirectory), "f"); const database = options.database || new fileDatabase_1.FileDatabase(__classPrivateFieldGet(this, _HLSController_fileStorage, "f")); __classPrivateFieldSet(this, _HLSController_qualityService, new qualityService_1.QualityService(options.videoQualities, options.audioQualities), "f"); __classPrivateFieldSet(this, _HLSController_metadataService, new metadataService_1.MetadataService(__classPrivateFieldGet(this, _HLSController_qualityService, "f"), database), "f"); __classPrivateFieldSet(this, _HLSController_clientTracker, new clientTracker_1.ClientTracker(__classPrivateFieldGet(this, _HLSController_qualityService, "f"), undefined, // inactivityCheckFrequency undefined, // unusedStreamDebounceDelay undefined), "f"); __classPrivateFieldSet(this, _HLSController_jobProcessor, new jobProcessor_1.JobProcessor(), "f"); } /** * Initialize the HLS manager * This will initialize the transcodeService and detect hardware acceleration */ async initialize() { __classPrivateFieldGet(this, _HLSController_instances, "m", _HLSController_hookUpClientTracker).call(this); __classPrivateFieldGet(this, _HLSController_instances, "m", _HLSController_hookUpJobProcessor).call(this); // Initialize segment processor if (__classPrivateFieldGet(this, _HLSController_distributedConfig, "f")?.enabled !== false) { __classPrivateFieldSet(this, _HLSController_segmentProcessor, await distributed_1.SegmentProcessorFactory.create({ redisUrl: __classPrivateFieldGet(this, _HLSController_distributedConfig, "f")?.redisUrl, workerId: __classPrivateFieldGet(this, _HLSController_distributedConfig, "f")?.workerId, claimTTL: __classPrivateFieldGet(this, _HLSController_distributedConfig, "f")?.claimTTL, fallbackToLocal: __classPrivateFieldGet(this, _HLSController_distributedConfig, "f")?.fallbackToLocal, }), "f"); const mode = __classPrivateFieldGet(this, _HLSController_segmentProcessor, "f").getMode(); console.log(`HLSController initialized with ${mode} segment processing`); } return __classPrivateFieldGet(this, _HLSController_hwDetector, "f").detectHardwareAcceleration() .filter(() => __classPrivateFieldGet(this, _HLSController_hwAccelEnabled, "f"), () => (0, fp_1.createBadRequestError)('Hardware acceleration is not supported on this system')) .map((hwConfig) => { __classPrivateFieldSet(this, _HLSController_hwConfig, hwConfig, "f"); }) .toPromise(); } /** * Get the master playlist for a media source * @param filePath The file path of the media source * @param clientId The client ID of the user requesting the stream */ getMasterPlaylist(filePath, clientId) { const source = __classPrivateFieldGet(this, _HLSController_instances, "m", _HLSController_buildMediaSource).call(this, filePath); return __classPrivateFieldGet(this, _HLSController_metadataService, "f").getMasterPlaylist(source) .ioSync((masterPlaylist) => { void __classPrivateFieldGet(this, _HLSController_instances, "m", _HLSController_prepareSegments).call(this, filePath, clientId, types_1.StreamType.VIDEO, masterPlaylist.video.quality, masterPlaylist.video.index, 0); void __classPrivateFieldGet(this, _HLSController_instances, "m", _HLSController_prepareSegments).call(this, filePath, clientId, types_1.StreamType.AUDIO, masterPlaylist.audio.quality, masterPlaylist.audio.index, 0); }) .map(({ master }) => master) .toPromise(); } /** * Get the playlist for a media source with the given stream type and quality * @param filePath The file path of the media source * @param clientId The client ID of the user requesting the stream * @param type The stream type * @param quality The stream quality * @param streamIndex The stream index */ getIndexPlaylist(filePath, clientId, type, quality, streamIndex) { return __classPrivateFieldGet(this, _HLSController_instances, "m", _HLSController_getStreamAndPriority).call(this, filePath, clientId, type, quality, streamIndex, 0) .ioSync(({ stream }) => __classPrivateFieldGet(this, _HLSController_clientTracker, "f").registerClientActivity({ clientId, filePath, segment: 0, audioIndex: type === types_1.StreamType.AUDIO ? streamIndex : undefined, videoIndex: type === types_1.StreamType.VIDEO ? streamIndex : undefined, videoQuality: type === types_1.StreamType.VIDEO ? quality : undefined, audioQuality: type === types_1.StreamType.AUDIO ? quality : undefined, fileId: stream.getFileId(), })) .ioSync(() => void __classPrivateFieldGet(this, _HLSController_instances, "m", _HLSController_prepareSegments).call(this, filePath, clientId, type, quality, streamIndex, 0)) .map(({ stream }) => stream.getPlaylist()) .toPromise(); } /** * Get the segment stream for a media source with the given stream type and quality * @param filePath The file path of the media source * @param clientId The client ID of the user requesting the stream * @param type The stream type * @param quality The stream quality * @param streamIndex The stream index * @param segmentNumber The segment number to get */ getSegmentStream(filePath, clientId, type, quality, streamIndex, segmentNumber) { return __classPrivateFieldGet(this, _HLSController_instances, "m", _HLSController_getStreamAndPriority).call(this, filePath, clientId, type, quality, streamIndex, segmentNumber) .ioSync(({ stream }) => __classPrivateFieldGet(this, _HLSController_clientTracker, "f").registerClientActivity({ clientId, filePath, segment: segmentNumber, audioIndex: type === types_1.StreamType.AUDIO ? streamIndex : undefined, videoIndex: type === types_1.StreamType.VIDEO ? streamIndex : undefined, videoQuality: type === types_1.StreamType.VIDEO ? quality : undefined, audioQuality: type === types_1.StreamType.AUDIO ? quality : undefined, fileId: stream.getFileId(), })) .chain(({ stream, priority }) => stream.getSegmentStream(segmentNumber, priority)) .toPromise(); } /** * Extract subtitle from a media source and convert to WebVTT * @param filePath The file path of the media source * @param streamIndex The subtitle stream index to extract * @returns TaskEither containing the VTT content as string */ getVTTSubtitle(filePath, streamIndex) { return fp_1.TaskEither .tryCatch(() => this.getVTTSubtitleStream(filePath, streamIndex)) .chain(utils_1.streamToString) .toPromise(); } /** * Extract subtitle from a media source and convert to WebVTT * @param filePath The file path of the media source * @param streamIndex The subtitle stream index to extract * @returns TaskEither containing the VTT content as stream */ getVTTSubtitleStream(filePath, streamIndex) { const mediaSource = __classPrivateFieldGet(this, _HLSController_instances, "m", _HLSController_buildMediaSource).call(this, filePath); return stream_1.Stream.getVTTSubtitle(mediaSource, streamIndex); } /** * Create a screenshot from a media source at a specific timestamp * @param filePath The file path of the media source * @param quality The quality of the screenshot * @param streamIndex The stream index to take the screenshot from * @param time The time to take the screenshot at */ generateScreenshot(filePath, quality, streamIndex, time) { return __classPrivateFieldGet(this, _HLSController_instances, "m", _HLSController_getOrCreateStream).call(this, filePath, types_1.StreamType.VIDEO, quality, streamIndex) .fromPromise((stream) => stream.generateScreenshot(time)) .toPromise(); } /** * Get all convertible subtitle streams from media metadata * @param filePath The file path of the media source */ getConvertibleSubtitles(filePath) { const mediaSource = __classPrivateFieldGet(this, _HLSController_instances, "m", _HLSController_buildMediaSource).call(this, filePath); return __classPrivateFieldGet(this, _HLSController_metadataService, "f").getMetadata(mediaSource) .map((metadata) => stream_1.Stream.getConvertibleSubtitles(metadata)) .toPromise(); } /** * Sets up a listener for when the client session changes * @param callback The callback to call when the session changes */ onSessionChange(callback) { __classPrivateFieldGet(this, _HLSController_clientTracker, "f").on('session:updated', (data) => { try { const streamId = stream_1.Stream.getStreamId(data.fileId, types_1.StreamType.VIDEO, data.videoQuality, data.videoIndex); const stream = __classPrivateFieldGet(this, _HLSController_streams, "f").get(streamId); if (!stream) { return; } const videoQuality = stream.buildVideoQuality(data.videoQuality, data.videoIndex); const audioQuality = stream.buildAudioQuality(data.audioQuality, data.audioIndex); const transcodeNumber = (videoQuality.value !== types_1.VideoQualityEnum.ORIGINAL ? 0 : 1) + (audioQuality.value !== types_1.AudioQualityEnum.ORIGINAL ? 0 : 1); const status = transcodeNumber === 2 ? types_1.TranscodeType.DIRECT_PLAY : transcodeNumber === 1 ? types_1.TranscodeType.DIRECT_STREAM : types_1.TranscodeType.TRANSCODING; const session = { status, filePath: data.filePath, clientId: data.clientId, audioIndex: data.audioIndex, videoIndex: data.videoIndex, audioProfile: audioQuality, videoProfile: videoQuality, }; return callback(session); } catch { // no-op } }); } /** * Sets up a listener for when the stream metrics change * @param callback The callback to call when the metrics change */ onStreamMetrics(callback) { __classPrivateFieldSet(this, _HLSController_streamMetricsEventHandler, callback, "f"); } /** * Get the current job processor status * @returns The current status of the job processor */ getJobProcessorStatus() { return __classPrivateFieldGet(this, _HLSController_jobProcessor, "f").getStatus(); } /** * Get the segment processor * @returns The segment processor instance or null */ getSegmentProcessor() { return __classPrivateFieldGet(this, _HLSController_segmentProcessor, "f"); } /** * Create metadata for a media source * @param filePath The file path of the media source */ createMetadata(filePath) { const source = __classPrivateFieldGet(this, _HLSController_instances, "m", _HLSController_buildMediaSource).call(this, filePath); return __classPrivateFieldGet(this, _HLSController_metadataService, "f").createMetadata(source).toPromise(); } /** * Dispose of the controller and clean up resources */ async dispose() { // Clear load adjustment interval if (__classPrivateFieldGet(this, _HLSController_loadAdjustmentInterval, "f")) { clearInterval(__classPrivateFieldGet(this, _HLSController_loadAdjustmentInterval, "f")); __classPrivateFieldSet(this, _HLSController_loadAdjustmentInterval, null, "f"); } // Dispose all streams for (const stream of __classPrivateFieldGet(this, _HLSController_streams, "f").values()) { void stream.dispose(); } __classPrivateFieldGet(this, _HLSController_streams, "f").clear(); // Dispose segment processor if (__classPrivateFieldGet(this, _HLSController_segmentProcessor, "f")) { await __classPrivateFieldGet(this, _HLSController_segmentProcessor, "f").dispose(); __classPrivateFieldSet(this, _HLSController_segmentProcessor, null, "f"); } // Dispose client tracker __classPrivateFieldGet(this, _HLSController_clientTracker, "f").dispose(); // Dispose job processor __classPrivateFieldGet(this, _HLSController_jobProcessor, "f").dispose(); } } exports.HLSController = HLSController; _HLSController_hwConfig = new WeakMap(), _HLSController_streamMetricsEventHandler = new WeakMap(), _HLSController_segmentProcessor = new WeakMap(), _HLSController_hwDetector = new WeakMap(), _HLSController_metadataService = new WeakMap(), _HLSController_qualityService = new WeakMap(), _HLSController_streams = new WeakMap(), _HLSController_clientTracker = new WeakMap(), _HLSController_maxSegmentBatchSize = new WeakMap(), _HLSController_fileStorage = new WeakMap(), _HLSController_hwAccelEnabled = new WeakMap(), _HLSController_jobProcessor = new WeakMap(), _HLSController_distributedConfig = new WeakMap(), _HLSController_loadAdjustmentInterval = new WeakMap(), _HLSController_instances = new WeakSet(), _HLSController_hookUpJobProcessor = function _HLSController_hookUpJobProcessor() { __classPrivateFieldGet(this, _HLSController_jobProcessor, "f").on('job:failed', ({ job, error }) => { console.error(`Transcode job failed: ${job.id}`, error); }); __classPrivateFieldGet(this, _HLSController_jobProcessor, "f").on('queue:full', ({ size, maxSize }) => { console.warn(`Job queue is full: ${size}/${maxSize}`); }); __classPrivateFieldGet(this, _HLSController_jobProcessor, "f").on('concurrency:changed', ({ current, max }) => { console.info(`Concurrency changed: ${current}/${max}`); }); // Optional: Periodically adjust concurrency based on system load __classPrivateFieldSet(this, _HLSController_loadAdjustmentInterval, setInterval(() => { __classPrivateFieldGet(this, _HLSController_jobProcessor, "f").adjustConcurrencyBasedOnLoad(); }, 30000), "f"); // Every 30 seconds }, _HLSController_hookUpClientTracker = function _HLSController_hookUpClientTracker() { __classPrivateFieldGet(this, _HLSController_clientTracker, "f").on('stream:abandoned', async ({ streamId }) => { const stream = __classPrivateFieldGet(this, _HLSController_streams, "f").get(streamId); if (stream) { await stream.dispose(); } }); }, _HLSController_hookUpStream = function _HLSController_hookUpStream(stream) { stream.on('dispose', ({ id }) => { __classPrivateFieldGet(this, _HLSController_streams, "f").delete(id); }); stream.on('stream:metrics', (event) => { if (__classPrivateFieldGet(this, _HLSController_streamMetricsEventHandler, "f")) { __classPrivateFieldGet(this, _HLSController_streamMetricsEventHandler, "f").call(this, event); } }); }, _HLSController_getOrCreateStream = function _HLSController_getOrCreateStream(filePath, type, quality, streamIndex) { const source = __classPrivateFieldGet(this, _HLSController_instances, "m", _HLSController_buildMediaSource).call(this, filePath); const createStream = () => stream_1.Stream .create(quality, type, streamIndex, source, __classPrivateFieldGet(this, _HLSController_maxSegmentBatchSize, "f"), __classPrivateFieldGet(this, _HLSController_qualityService, "f"), __classPrivateFieldGet(this, _HLSController_metadataService, "f"), __classPrivateFieldGet(this, _HLSController_hwDetector, "f"), __classPrivateFieldGet(this, _HLSController_hwConfig, "f"), this.streamConfig, __classPrivateFieldGet(this, _HLSController_jobProcessor, "f")) .ioSync((stream) => __classPrivateFieldGet(this, _HLSController_instances, "m", _HLSController_hookUpStream).call(this, stream)) .ioSync((stream) => { __classPrivateFieldGet(this, _HLSController_streams, "f").set(stream.getStreamId(), stream); }); return source.getFileId() .chain((fileId) => { const streamId = stream_1.Stream.getStreamId(fileId, type, quality, streamIndex); const localStream = __classPrivateFieldGet(this, _HLSController_streams, "f").get(streamId); if (localStream) { return fp_1.TaskEither.of(localStream); } return createStream(); }); }, _HLSController_getStreamAndPriority = function _HLSController_getStreamAndPriority(filePath, clientId, type, quality, streamIndex, segmentNumber) { return fp_1.TaskEither .fromBind({ stream: __classPrivateFieldGet(this, _HLSController_instances, "m", _HLSController_getOrCreateStream).call(this, filePath, type, quality, streamIndex), priority: __classPrivateFieldGet(this, _HLSController_clientTracker, "f").getPriority(clientId, type, quality, segmentNumber), }); }, _HLSController_buildMediaSource = function _HLSController_buildMediaSource(filePath) { return new mediaSource_1.MediaSource(filePath, __classPrivateFieldGet(this, _HLSController_fileStorage, "f")); }, _HLSController_prepareSegments = function _HLSController_prepareSegments(filePath, clientId, type, quality, streamIndex, segmentNumber) { return __classPrivateFieldGet(this, _HLSController_instances, "m", _HLSController_getStreamAndPriority).call(this, filePath, clientId, type, quality, streamIndex, segmentNumber) .chain(({ stream, priority }) => stream.buildTranscodeCommand(segmentNumber, priority)) .toResult(); }; //# sourceMappingURL=hlsController.js.map