@eleven-am/transcoder
Version:
High-performance HLS transcoding library with hardware acceleration, intelligent client management, and distributed processing support for Node.js
247 lines • 9.56 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/>.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.RedisSegmentClaimManager = void 0;
/**
* Manages distributed segment claims using Redis
* Ensures only one worker processes each segment at a time
*/
class RedisSegmentClaimManager {
constructor(redis, workerId, defaultTTL = 60000, // 60 seconds
completedSegmentTTL) {
this.redis = redis;
this.workerId = workerId;
this.defaultTTL = defaultTTL;
this.lockPrefix = 'transcoder:segment:lock:';
this.statusPrefix = 'transcoder:segment:status:';
this.completedPrefix = 'transcoder:segment:completed:';
this.subscriberPool = [];
this.poolSize = 5;
this.disposed = false;
this.completedSegmentTTL = completedSegmentTTL || 7 * 24 * 60 * 60 * 1000;
}
/**
* Try to claim a segment for processing
*/
async claimSegment(fileId, streamType, quality, streamIndex, segmentIndex) {
const segmentKey = this.getSegmentKey(fileId, streamType, quality, streamIndex, segmentIndex);
const lockKey = `${this.lockPrefix}${segmentKey}`;
const expiresAt = Date.now() + this.defaultTTL;
// Try to acquire lock atomically
const acquired = await this.redis.set(lockKey, JSON.stringify({ workerId: this.workerId,
expiresAt }), { NX: true,
PX: this.defaultTTL });
if (!acquired) {
return this.createFailedClaim(segmentKey);
}
// Mark segment as processing
await this.redis.set(`${this.statusPrefix}${segmentKey}`, 'processing', { PX: this.defaultTTL * 2 });
return this.createSuccessfulClaim(segmentKey, lockKey, expiresAt);
}
/**
* Check if a segment is already completed
*/
async isSegmentCompleted(fileId, streamType, quality, streamIndex, segmentIndex) {
const segmentKey = this.getSegmentKey(fileId, streamType, quality, streamIndex, segmentIndex);
const completed = await this.redis.get(`${this.completedPrefix}${segmentKey}`);
return completed === 'true';
}
/**
* Mark a segment as completed
*/
async markSegmentCompleted(fileId, streamType, quality, streamIndex, segmentIndex) {
const segmentKey = this.getSegmentKey(fileId, streamType, quality, streamIndex, segmentIndex);
// Set completed status with configurable TTL
await this.redis.set(`${this.completedPrefix}${segmentKey}`, 'true', { PX: this.completedSegmentTTL });
// Update status
await this.redis.set(`${this.statusPrefix}${segmentKey}`, 'completed', { PX: this.completedSegmentTTL });
}
/**
* Get the status of a segment
*/
async getSegmentStatus(fileId, streamType, quality, streamIndex, segmentIndex) {
const segmentKey = this.getSegmentKey(fileId, streamType, quality, streamIndex, segmentIndex);
return await this.redis.get(`${this.statusPrefix}${segmentKey}`);
}
/**
* Publish segment completion event
*/
async publishSegmentComplete(fileId, streamType, quality, streamIndex, segmentIndex) {
const segmentKey = this.getSegmentKey(fileId, streamType, quality, streamIndex, segmentIndex);
const channel = `transcoder:segment:complete:${segmentKey}`;
await this.redis.publish(channel, 'completed');
}
/**
* Get a subscriber from the pool or create a new one
*/
async getSubscriber() {
// Try to get from pool first
const subscriber = this.subscriberPool.pop();
if (subscriber && subscriber.isOpen) {
return subscriber;
}
// Create new subscriber if pool is empty or subscriber was closed
const newSubscriber = this.redis.duplicate();
await newSubscriber.connect();
return newSubscriber;
}
/**
* Release a subscriber back to the pool or disconnect it
*/
async releaseSubscriber(subscriber) {
if (this.disposed || !subscriber.isOpen) {
// Always disconnect if disposed or subscriber is not open
try {
await subscriber.disconnect();
}
catch {
// Ignore disconnect errors
}
return;
}
if (this.subscriberPool.length < this.poolSize) {
// Return to pool if there's space
this.subscriberPool.push(subscriber);
}
else {
// Disconnect if pool is full
try {
await subscriber.disconnect();
}
catch {
// Ignore disconnect errors
}
}
}
/**
* Subscribe to segment completion events
*/
async subscribeToSegmentComplete(fileId, streamType, quality, streamIndex, segmentIndex, callback) {
const segmentKey = this.getSegmentKey(fileId, streamType, quality, streamIndex, segmentIndex);
const channel = `transcoder:segment:complete:${segmentKey}`;
const subscriber = await this.getSubscriber();
try {
await subscriber.subscribe(channel, (message) => {
if (message === 'completed') {
callback();
}
});
}
catch (error) {
await this.releaseSubscriber(subscriber);
throw error;
}
// Return unsubscribe function
return async () => {
try {
if (subscriber.isOpen) {
await subscriber.unsubscribe(channel);
}
}
catch (err) {
console.error('Error during Redis unsubscribe:', err);
}
finally {
// Always release the subscriber
await this.releaseSubscriber(subscriber);
}
};
}
/**
* Dispose of the manager and clean up resources
*/
async dispose() {
this.disposed = true;
// Disconnect all pooled subscribers
const subscribers = [...this.subscriberPool];
this.subscriberPool.length = 0;
await Promise.all(subscribers.map(async (subscriber) => {
try {
if (subscriber.isOpen) {
await subscriber.disconnect();
}
}
catch {
// Ignore disconnect errors
}
}));
}
getSegmentKey(fileId, streamType, quality, streamIndex, segmentIndex) {
return `${fileId}:${streamType}:${quality}:${streamIndex}:${segmentIndex}`;
}
createFailedClaim(segmentKey) {
return {
acquired: false,
segmentKey,
workerId: this.workerId,
expiresAt: 0,
extend: async () => false,
release: async () => { },
};
}
createSuccessfulClaim(segmentKey, lockKey, expiresAt) {
return {
acquired: true,
segmentKey,
workerId: this.workerId,
expiresAt,
extend: async () => {
// Extend lock using Lua script for atomicity
const script = `
local lock = redis.call('get', KEYS[1])
if lock then
local data = cjson.decode(lock)
if data.workerId == ARGV[1] then
local newExpiry = tonumber(ARGV[2])
data.expiresAt = newExpiry
redis.call('set', KEYS[1], cjson.encode(data), 'PX', ARGV[3])
return 1
end
end
return 0
`;
const newExpiresAt = Date.now() + this.defaultTTL;
const result = await this.redis.eval(script, {
keys: [lockKey],
arguments: [this.workerId, newExpiresAt.toString(), this.defaultTTL.toString()],
});
return result === 1;
},
release: async () => {
// Release lock only if we own it
const script = `
local lock = redis.call('get', KEYS[1])
if lock then
local data = cjson.decode(lock)
if data.workerId == ARGV[1] then
return redis.call('del', KEYS[1])
end
end
return 0
`;
await this.redis.eval(script, {
keys: [lockKey],
arguments: [this.workerId],
});
},
};
}
}
exports.RedisSegmentClaimManager = RedisSegmentClaimManager;
//# sourceMappingURL=redisSegmentClaimManager.js.map