UNPKG

@moartube/moartube-client

Version:

A free, open-source, self-hosted, anonymous, decentralized video/live stream platform. Scalable via Cloudflare, works in the cloud or from home WiFi.

551 lines (421 loc) 17.5 kB
const path = require('path'); const fs = require('fs'); const fss = require('fs').promises; const webSocket = require('ws'); const axios = require('axios').default; let isDeveloperMode; let publicDirectoryPath; let viewsDirectoryPath; let dataDirectoryPath; let videosDirectoryPath; let moartubeClientPort; let moartubeNodeIp; let moartubeNodePort; let moartubeNodeHttpProtocol; let moartubeNodeWebsocketProtocol; let ffmpegPath; let websocketServer; let websocketClient; let nodeSettings; let externalVideosBaseUrl; function logDebugMessageToConsole(message, error, stackTrace) { const date = new Date(Date.now()); const year = date.getFullYear(); const month = ('0' + (date.getMonth() + 1)).slice(-2); const day = ('0' + date.getDate()).slice(-2); const hours = ('0' + date.getHours()).slice(-2); const minutes = ('0' + date.getMinutes()).slice(-2); const seconds = ('0' + date.getSeconds()).slice(-2); const humanReadableTimestamp = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; let errorMessage = '<message: ' + message + ', date: ' + humanReadableTimestamp + '>'; if (error != null) { if (error.stack != null) { errorMessage += '\n' + error.stack; } else if (error.stackTrace != null) { errorMessage += '\n' + error.stackTrace; } } if (stackTrace != null) { errorMessage += '\n' + stackTrace; } console.log(errorMessage); errorMessage += '\n'; /* if(isLoggingToFile) { const logFilePath = path.join(__dirname, '/_node_log.txt'); fs.appendFileSync(logFilePath, errorMessage); } */ } async function deleteDirectoryRecursive(directoryPath) { try { await fss.rm(directoryPath, { recursive: true, force: true }); } catch (error) { logDebugMessageToConsole('failed to delete directory path: ' + directoryPath, error, null); } } function timestampToSeconds(timestamp) { const parts = timestamp.split(':'); const hours = parseInt(parts[0]); const minutes = parseInt(parts[1]); const seconds = parseFloat(parts[2]); return (hours * 3600) + (minutes * 60) + seconds; } function detectOperatingSystem() { const os = require('os'); const platform = os.platform(); return platform; } async function detectSystemGpu() { const systemInformation = require('systeminformation'); const graphics = await systemInformation.graphics(); let processingAgentName = ''; let processingAgentModel = ''; for (const controller of graphics.controllers) { if (controller.vendor.toLowerCase().includes('nvidia')) { processingAgentName = 'NVIDIA'; processingAgentModel = controller.model.replace(/^.*\bNVIDIA\s*/, ''); break; } else if (controller.vendor.toLowerCase().includes('amd') || controller.vendor.toLowerCase().includes('advanced micro devices')) { processingAgentName = 'AMD'; processingAgentModel = controller.model.replace(/^.*\bAMD\s*/, ''); break; } else { processingAgentName = 'none'; processingAgentModel = 'none'; break; } } return { processingAgentName: processingAgentName, processingAgentModel: processingAgentModel }; } async function detectSystemCpu() { const systemInformation = require('systeminformation'); const cpu = await systemInformation.cpu(); const processingAgentName = cpu.manufacturer; const processingAgentModel = cpu.brand; return { processingAgentName: processingAgentName, processingAgentModel: processingAgentModel }; } function getNetworkAddresses() { const os = require('os'); const networkInterfaces = os.networkInterfaces(); const ipv4Addresses = ['127.0.0.1']; const ipv6Addresses = ['::1']; for (const networkInterfaceKey of Object.keys(networkInterfaces)) { const networkInterface = networkInterfaces[networkInterfaceKey]; for (const networkInterfaceElement of networkInterface) { const networkAddress = networkInterfaceElement.address; if (networkInterfaceElement.family === 'IPv4' && networkAddress !== '127.0.0.1') { ipv4Addresses.push(networkAddress); } else if (networkInterfaceElement.family === 'IPv6' && networkAddress !== '::1') { ipv6Addresses.push(networkAddress); } } } const networkAddresses = ipv4Addresses.concat(ipv6Addresses); return networkAddresses; } async function performEncodingDecodingAssessment() { logDebugMessageToConsole('assessing system encoding/decoding capabilities', null, null); const systemCpu = await detectSystemCpu(); const systemGpu = await detectSystemGpu(); logDebugMessageToConsole('CPU detected: ' + systemCpu.processingAgentName + ' ' + systemCpu.processingAgentModel, null, null); logDebugMessageToConsole('GPU detected: ' + systemGpu.processingAgentName + ' ' + systemGpu.processingAgentModel, null, null); } async function cleanVideosDirectory() { logDebugMessageToConsole('cleaning imported video directories', null, null); const videosDirectoryPath = getVideosDirectoryPath(); await fss.access(videosDirectoryPath).catch(() => { throw new Error(`expected path does not exist: ${videosDirectoryPath}`); }); const videoDirectories = await fss.readdir(videosDirectoryPath); for (const videoDirectory of videoDirectories) { const videoDirectoryPath = path.join(videosDirectoryPath, videoDirectory); const stat = await fss.stat(videoDirectoryPath); if (!stat.isDirectory()) { throw new Error(`expected path is not a directory: ${videoDirectoryPath}`); } const directories = await fss.readdir(videoDirectoryPath); for (const directory of directories) { if (directory !== 'source') { const directoryPath = path.join(videoDirectoryPath, directory); await deleteDirectoryRecursive(directoryPath); } } } } async function refreshM3u8MasterManifest(jwtToken, videoId) { const { node_getVideoData, node_uploadM3u8MasterManifest } = require('./node-communications'); const { s3_putObjectFromData } = require('./s3-communications'); const videoData = (await node_getVideoData(videoId)).videoData; const isStreaming = videoData.isStreaming; const resolutions = videoData.outputs.m3u8; const externalVideosBaseUrl = await getExternalVideosBaseUrl(jwtToken); let manifestType; if (isStreaming) { manifestType = 'dynamic'; } else { manifestType = 'static'; } let masterManifest = '#EXTM3U\n#EXT-X-VERSION:3\n'; for (const resolution of resolutions) { if (resolution === '240p') { masterManifest += '#EXT-X-STREAM-INF:BANDWIDTH=250000,RESOLUTION=426x240\n'; masterManifest += externalVideosBaseUrl + '/external/videos/' + videoId + '/adaptive/m3u8/' + manifestType + '/manifests/manifest-240p.m3u8\n'; } else if (resolution === '360p') { masterManifest += '#EXT-X-STREAM-INF:BANDWIDTH=500000,RESOLUTION=640x360\n'; masterManifest += externalVideosBaseUrl + '/external/videos/' + videoId + '/adaptive/m3u8/' + manifestType + '/manifests/manifest-360p.m3u8\n'; } else if (resolution === '480p') { masterManifest += '#EXT-X-STREAM-INF:BANDWIDTH=1000000,RESOLUTION=854x480\n'; masterManifest += externalVideosBaseUrl + '/external/videos/' + videoId + '/adaptive/m3u8/' + manifestType + '/manifests/manifest-480p.m3u8\n'; } else if (resolution === '720p') { masterManifest += '#EXT-X-STREAM-INF:BANDWIDTH=3000000,RESOLUTION=1280x720\n'; masterManifest += externalVideosBaseUrl + '/external/videos/' + videoId + '/adaptive/m3u8/' + manifestType + '/manifests/manifest-720p.m3u8\n'; } else if (resolution === '1080p') { masterManifest += '#EXT-X-STREAM-INF:BANDWIDTH=6000000,RESOLUTION=1920x1080\n'; masterManifest += externalVideosBaseUrl + '/external/videos/' + videoId + '/adaptive/m3u8/' + manifestType + '/manifests/manifest-1080p.m3u8\n'; } else if (resolution === '1440p') { masterManifest += '#EXT-X-STREAM-INF:BANDWIDTH=8000000,RESOLUTION=2560x1440\n'; masterManifest += externalVideosBaseUrl + '/external/videos/' + videoId + '/adaptive/m3u8/' + manifestType + '/manifests/manifest-1440p.m3u8\n'; } else if (resolution === '2160p') { masterManifest += '#EXT-X-STREAM-INF:BANDWIDTH=16000000,RESOLUTION=3840x2160\n' masterManifest += externalVideosBaseUrl + '/external/videos/' + videoId + '/adaptive/m3u8/' + manifestType + '/manifests/manifest-2160p.m3u8\n'; } } const nodeSettings = await getNodeSettings(jwtToken); const storageConfig = nodeSettings.storageConfig; const storageMode = storageConfig.storageMode; if (storageMode === 'filesystem') { await node_uploadM3u8MasterManifest(jwtToken, videoId, manifestType, masterManifest); } else if (storageMode === 's3provider') { const s3Config = storageConfig.s3Config; const key = 'external/videos/' + videoId + '/adaptive/m3u8/' + manifestType + '/manifests/manifest-master.m3u8'; await s3_putObjectFromData(s3Config, key, Buffer.from(masterManifest), 'application/vnd.apple.mpegurl'); } } function setFfmpegPath(value) { if (fs.existsSync(value)) { ffmpegPath = value; const execSync = require('child_process').execSync; logDebugMessageToConsole('using ffmpeg at path: ' + ffmpegPath, null, null); logDebugMessageToConsole(execSync(getFfmpegPath() + ' -version').toString(), null, null); } else { throw new Error('ffmpeg does not exist at path: ' + value); } } function getFfmpegPath() { return ffmpegPath; } function websocketClientBroadcast(message) { if (websocketClient != null) { websocketClient.send(JSON.stringify(message)); } } function websocketServerBroadcast(message) { websocketServer.clients.forEach(function each(client) { if (client.readyState === webSocket.OPEN) { client.send(JSON.stringify(message)); } }); } /* getters */ function getPublicDirectoryPath() { return publicDirectoryPath; } function getViewsDirectoryPath() { return viewsDirectoryPath; } function getDataDirectoryPath() { return dataDirectoryPath; } function getVideosDirectoryPath() { return videosDirectoryPath; } function getMoarTubeClientPort() { return moartubeClientPort; } function getMoarTubeNodeIp() { return moartubeNodeIp; } function getMoarTubeNodePort() { return moartubeNodePort; } function getMoarTubeNodeHttpProtocol() { return moartubeNodeHttpProtocol; } function getMoarTubeNodeWebsocketProtocol() { return moartubeNodeWebsocketProtocol; } function getMoarTubeNodeUrl() { return (getMoarTubeNodeHttpProtocol() + '://' + getMoarTubeNodeIp() + ':' + getMoarTubeNodePort()); } function getMoarTubeNodeWebsocketUrl() { return (getMoarTubeNodeWebsocketProtocol() + '://' + getMoarTubeNodeIp() + ':' + getMoarTubeNodePort()); } function getIsDeveloperMode() { return isDeveloperMode; } function getClientSettings() { const clientSettings = JSON.parse(fs.readFileSync(path.join(getDataDirectoryPath(), '_client_settings.json'), 'utf8')); return clientSettings; } function getClientSettingsDefault() { const clientSettingsDefault = JSON.parse(fs.readFileSync(path.join(getDataDirectoryPath(), '_client_settings_default.json'), 'utf8')); return clientSettingsDefault; } function getWebsocketServer() { return websocketServer; } function getWebsocketClient() { return websocketClient; } /* setters */ function setPublicDirectoryPath(path) { logDebugMessageToConsole('configured MoarTube Client to use public directory path: ' + path, null, null); publicDirectoryPath = path; } function setViewsDirectoryPath(path) { viewsDirectoryPath = path; } function setDataDirectoryPath(path) { logDebugMessageToConsole('configured MoarTube Client to use data directory path: ' + path, null, null); dataDirectoryPath = path; } function setVideosDirectoryPath(path) { logDebugMessageToConsole('configured MoarTube Client to use videos directory path: ' + path, null, null); videosDirectoryPath = path; } function setMoarTubeClientPort(port) { logDebugMessageToConsole('configured MoarTube Client to use port: ' + port, null, null); moartubeClientPort = port; } function setMoarTubeNodeIp(ip) { logDebugMessageToConsole('configured MoarTube Client to use MoarTube Node ip: ' + ip, null, null); moartubeNodeIp = ip; } function setMoarTubeNodePort(port) { logDebugMessageToConsole('configured MoarTube Client to use MoarTube Node port: ' + port, null, null); moartubeNodePort = port; } function setMoarTubeNodeHttpProtocol(httpProtocol) { logDebugMessageToConsole('configured MoarTube Client to use MoarTube Node http protocol: ' + httpProtocol, null, null); moartubeNodeHttpProtocol = httpProtocol; } function setMoarTubeNodeWebsocketProtocol(websocketprotocol) { logDebugMessageToConsole('configured MoarTube Client to use MoarTube Node websocket protocol: ' + websocketprotocol, null, null); moartubeNodeWebsocketProtocol = websocketprotocol; } function setIsDeveloperMode(value) { isDeveloperMode = value; } function setClientSettings(clientSettings) { const clientSettingsString = JSON.stringify(clientSettings); logDebugMessageToConsole('configured MoarTube Client to use client settings: ' + clientSettingsString, null, null); fs.writeFileSync(path.join(getDataDirectoryPath(), '_client_settings.json'), clientSettingsString); } function setWebsocketServer(wss) { logDebugMessageToConsole('configured MoarTube Client with websocket server', null, null); websocketServer = wss; } function setWebsocketClient(wsc) { logDebugMessageToConsole('configured MoarTube Client with websocket client', null, null); websocketClient = wsc; } function cacheM3u8Segment(segmentFileUrl) { axios.get(segmentFileUrl); } async function getNodeSettings(jwtToken) { if (nodeSettings == null) { const { node_getSettings } = require('./node-communications'); nodeSettings = (await node_getSettings(jwtToken)).nodeSettings; logDebugMessageToConsole('retrieved nodeSettings from MoarTube Node', null, null); } return nodeSettings; } function clearNodeSettingsClientCache() { nodeSettings = null; } async function getExternalVideosBaseUrl(jwtToken) { if (externalVideosBaseUrl == null) { const { node_getExternalVideosBaseUrl } = require('./node-communications'); externalVideosBaseUrl = (await node_getExternalVideosBaseUrl(jwtToken)).externalVideosBaseUrl; logDebugMessageToConsole('retrieved externalVideosBaseUrl from MoarTube Node', null, null); } return externalVideosBaseUrl; } function clearExternalVideosBaseUrlClientCache() { externalVideosBaseUrl = null; } async function checkNetworkPortStatus(port, host) { const portscanner = require('portscanner'); const portStatus = await portscanner.checkPortStatus(port, host); return portStatus; } module.exports = { logDebugMessageToConsole, deleteDirectoryRecursive, timestampToSeconds, detectOperatingSystem, detectSystemGpu, detectSystemCpu, getNetworkAddresses, performEncodingDecodingAssessment, cleanVideosDirectory, websocketClientBroadcast, websocketServerBroadcast, getPublicDirectoryPath, getViewsDirectoryPath, getDataDirectoryPath, getVideosDirectoryPath, getMoarTubeClientPort, getMoarTubeNodeIp, getMoarTubeNodePort, getMoarTubeNodeHttpProtocol, getMoarTubeNodeWebsocketProtocol, getMoarTubeNodeUrl, getMoarTubeNodeWebsocketUrl, getClientSettings, getClientSettingsDefault, getFfmpegPath, getWebsocketServer, getWebsocketClient, setPublicDirectoryPath, setViewsDirectoryPath, setDataDirectoryPath, setVideosDirectoryPath, setMoarTubeClientPort, setMoarTubeNodeIp, setMoarTubeNodePort, setMoarTubeNodeHttpProtocol, setMoarTubeNodeWebsocketProtocol, setClientSettings, setFfmpegPath, setWebsocketServer, setWebsocketClient, getIsDeveloperMode, setIsDeveloperMode, refreshM3u8MasterManifest, cacheM3u8Segment, getNodeSettings, clearNodeSettingsClientCache, getExternalVideosBaseUrl, clearExternalVideosBaseUrlClientCache, checkNetworkPortStatus };