@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
JavaScript
"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