2025-06-10 14:23:06 -05:00
|
|
|
import * as fs from 'fs';
|
2025-06-11 14:51:55 -05:00
|
|
|
import * as url from 'url';
|
2025-06-10 14:23:06 -05:00
|
|
|
import { http, https } from 'modules/follow-redirects';
|
|
|
|
import * as memfs from 'modules/memfs';
|
|
|
|
import { Logger, LoggerType } from 'common/Logger';
|
2025-06-13 11:03:42 -05:00
|
|
|
import { supportedPlayerTypes } from 'common/MimeTypes';
|
|
|
|
import { NetworkService } from 'common/NetworkService';
|
|
|
|
import { ContentObject, ContentType, PlaylistContent, PlayMessage } from 'common/Packets';
|
2025-06-10 14:23:06 -05:00
|
|
|
const logger = new Logger('UtilityBackend', LoggerType.BACKEND);
|
|
|
|
|
|
|
|
export function deepEqual(x, y) {
|
|
|
|
const ok = Object.keys, tx = typeof x, ty = typeof y;
|
|
|
|
return x && y && tx === 'object' && tx === ty ? (
|
|
|
|
ok(x).length === ok(y).length &&
|
|
|
|
ok(x).every(key => deepEqual(x[key], y[key]))
|
|
|
|
) : (x === y);
|
|
|
|
}
|
|
|
|
|
2025-06-13 11:03:42 -05:00
|
|
|
export async function preparePlayMessage(message: PlayMessage, cachedPlayerVolume: number, mediaCacheInitializationCb: ((playMessage: PlaylistContent) => void)) {
|
|
|
|
// Protocol v2 FCast PlayMessage does not contain volume field and could result in the receiver
|
|
|
|
// getting out-of-sync with the sender when player windows are closed and re-opened. Volume
|
|
|
|
// is cached in the play message when volume is not set in v3 PlayMessage.
|
|
|
|
message.volume = message.volume === undefined ? cachedPlayerVolume : message.volume;
|
|
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
let rendererMessage: any = await NetworkService.proxyPlayIfRequired(message);
|
|
|
|
let rendererEvent = 'play';
|
|
|
|
let contentViewer = supportedPlayerTypes.find(v => v === message.container.toLocaleLowerCase()) ? 'player' : 'viewer';
|
|
|
|
|
|
|
|
if (message.container === 'application/json') {
|
|
|
|
const json: ContentObject = message.url ? await fetchJSON(message.url) : JSON.parse(message.content);
|
|
|
|
|
|
|
|
if (json && json.contentType !== undefined) {
|
|
|
|
switch (json.contentType) {
|
|
|
|
case ContentType.Playlist: {
|
|
|
|
rendererMessage = json as PlaylistContent;
|
|
|
|
rendererEvent = 'play-playlist';
|
|
|
|
|
|
|
|
if ((rendererMessage.forwardCache && rendererMessage.forwardCache > 0) || (rendererMessage.backwardCache && rendererMessage.backwardCache > 0)) {
|
|
|
|
mediaCacheInitializationCb(rendererMessage);
|
|
|
|
}
|
|
|
|
|
|
|
|
const offset = rendererMessage.offset ? rendererMessage.offset : 0;
|
|
|
|
contentViewer = supportedPlayerTypes.find(v => v === rendererMessage.items[offset].container.toLocaleLowerCase()) ? 'player' : 'viewer';
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return { rendererEvent: rendererEvent, rendererMessage: rendererMessage, contentViewer: contentViewer };
|
|
|
|
}
|
|
|
|
|
2025-06-10 14:23:06 -05:00
|
|
|
export async function fetchJSON(url: string): Promise<any> {
|
|
|
|
const protocol = url.startsWith('https') ? https : http;
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
protocol.get(url, (res) => {
|
|
|
|
let data = '';
|
|
|
|
res.on('data', (chunk) => {
|
|
|
|
data += chunk;
|
|
|
|
});
|
|
|
|
|
|
|
|
res.on('end', () => {
|
|
|
|
try {
|
|
|
|
resolve(JSON.parse(data));
|
|
|
|
} catch (err) {
|
|
|
|
reject(err);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}).on('error', (err) => {
|
|
|
|
reject(err);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2025-06-11 14:51:55 -05:00
|
|
|
export async function downloadFile(downloadUrl: string, destination: string, inMemory: boolean = false, requestHeaders: { [key: string]: string } = null,
|
|
|
|
startCb: (downloadSize: number) => boolean = null,
|
|
|
|
progressCb: (downloadedBytes: number, downloadSize: number) => void = null): Promise<void> {
|
2025-06-10 14:23:06 -05:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const file = inMemory ? memfs.fs.createWriteStream(destination) : fs.createWriteStream(destination);
|
2025-06-11 14:51:55 -05:00
|
|
|
const protocol = downloadUrl.startsWith('https') ? https : http;
|
2025-06-10 14:23:06 -05:00
|
|
|
|
2025-06-11 14:51:55 -05:00
|
|
|
const parsedUrl = url.parse(downloadUrl);
|
|
|
|
const options = protocol.RequestOptions = {
|
|
|
|
...parsedUrl,
|
|
|
|
headers: requestHeaders
|
|
|
|
};
|
|
|
|
|
|
|
|
protocol.get(options, (response) => {
|
2025-06-10 14:23:06 -05:00
|
|
|
const downloadSize = Number(response.headers['content-length']);
|
2025-06-11 14:51:55 -05:00
|
|
|
logger.info(`Downloading file ${downloadUrl} to ${destination} with size: ${downloadSize} bytes`);
|
2025-06-10 14:23:06 -05:00
|
|
|
if (startCb) {
|
|
|
|
if (!startCb(downloadSize)) {
|
|
|
|
file.close();
|
|
|
|
reject('Error: Aborted download');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
response.pipe(file);
|
|
|
|
let downloadedBytes = 0;
|
|
|
|
|
|
|
|
response.on('data', (chunk) => {
|
|
|
|
downloadedBytes += chunk.length;
|
|
|
|
if (progressCb) {
|
|
|
|
progressCb(downloadedBytes, downloadSize);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
file.on('finish', () => {
|
|
|
|
file.close();
|
|
|
|
resolve();
|
|
|
|
});
|
|
|
|
}).on('error', (err) => {
|
|
|
|
file.close();
|
|
|
|
reject(err);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|