@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.
533 lines (390 loc) • 21.1 kB
JavaScript
const path = require('path');
const fs = require('fs');
const spawnSync = require('child_process').spawnSync;
const sharp = require('sharp');
sharp.cache(false);
const {
logDebugMessageToConsole, deleteDirectoryRecursive, getVideosDirectoryPath, timestampToSeconds, websocketClientBroadcast, getFfmpegPath,
refreshM3u8MasterManifest, getNodeSettings
} = require('../utils/helpers');
const {
node_stopVideoImporting, node_doVideosSearch, node_getVideoData, node_unpublishVideo, node_stopVideoPublishing, node_setSourceFileExtension, node_setThumbnail,
node_setPreview, node_setPoster, node_setVideoLengths, node_setVideoImported, node_getVideosTags, node_getSourceFileExtension, node_getVideosTagsAll,
node_getVideoPublishes, node_setVideoData, node_deleteVideos, node_finalizeVideos, node_addVideoToIndex, node_removeVideoFromIndex, node_getVideoSources,
node_setIsIndexOutdated, node_getVideoPermissions, node_postVideoPermissions
} = require('../utils/node-communications');
const {
s3_putObjectFromData, s3_deleteObjectsWithPrefix, s3_deleteObjectWithKey
} = require('../utils/s3-communications');
const {
enqueuePendingPublishVideo
} = require('../utils/trackers/pending-publish-video-tracker');
async function search_GET(jwtToken, searchTerm, sortTerm, tagTerm, tagLimit, timestamp) {
const response = await node_doVideosSearch(jwtToken, searchTerm, sortTerm, tagTerm, tagLimit, timestamp);
return response;
}
async function import_POST(jwtToken, videoId, videoFile) {
let result;
if (videoFile != null && videoFile.length === 1) {
videoFile = videoFile[0];
const videoFilePath = videoFile.path;
const mimetype = videoFile.mimetype;
let sourceFileExtension;
if (mimetype === 'video/mp4') {
sourceFileExtension = '.mp4';
}
else if (mimetype === 'video/webm') {
sourceFileExtension = '.webm';
}
else {
throw new Error('unexpected source file: ' + sourceFileExtension);
}
const output = spawnSync(getFfmpegPath(), ['-i', videoFilePath], { encoding: 'utf-8' });
const durationIndex = output.stderr.indexOf('Duration: ');
const lengthTimestamp = output.stderr.substr(durationIndex + 10, 11);
const lengthSeconds = timestampToSeconds(lengthTimestamp);
const imageExtractionTimestamp = Math.floor(lengthSeconds * 0.25);
logDebugMessageToConsole('uploading video length to node for video: ' + videoId, null, null);
await node_setVideoLengths(jwtToken, videoId, lengthSeconds, lengthTimestamp);
logDebugMessageToConsole('uploaded video length to node for video: ' + videoId, null, null);
logDebugMessageToConsole('setting source file extension for video: ' + videoId, null, null);
await node_setSourceFileExtension(jwtToken, videoId, sourceFileExtension);
logDebugMessageToConsole('set source file extension for video: ' + videoId, null, null);
logDebugMessageToConsole('generating images for video: ' + videoId, null, null);
const imagesDirectoryPath = path.join(getVideosDirectoryPath(), videoId + '/images');
fs.mkdirSync(imagesDirectoryPath, { recursive: true });
const sourceImagePath = path.join(imagesDirectoryPath, 'source.jpg');
spawnSync(getFfmpegPath(), ['-ss', imageExtractionTimestamp, '-i', videoFilePath, sourceImagePath]);
logDebugMessageToConsole('generating thumbnail, preview, and poster for video: ' + videoId, null, null);
const thumbnailBuffer = await sharp(sourceImagePath).resize({ width: 100 }).resize(100, 100).jpeg({ quality: 90 }).toBuffer();
const previewFileBuffer = await sharp(sourceImagePath).resize({ width: 512 }).resize(512, 288).jpeg({ quality: 90 }).toBuffer();
const posterFileBuffer = await sharp(sourceImagePath).resize({ width: 1280 }).resize(1280, 720).jpeg({ quality: 90 }).toBuffer();
logDebugMessageToConsole('generated thumbnail, preview, and poster for video: ' + videoId, null, null);
const nodeSettings = await getNodeSettings(jwtToken);
const storageConfig = nodeSettings.storageConfig;
const storageMode = storageConfig.storageMode;
logDebugMessageToConsole('uploading thumbnail, preview, and poster for video: ' + videoId, null, null);
if (storageMode === 'filesystem') {
await node_setThumbnail(jwtToken, videoId, thumbnailBuffer);
logDebugMessageToConsole('uploaded thumbnail to node for video: ' + videoId, null, null);
await node_setPreview(jwtToken, videoId, previewFileBuffer);
logDebugMessageToConsole('uploaded preview to node for video: ' + videoId, null, null);
await node_setPoster(jwtToken, videoId, posterFileBuffer);
logDebugMessageToConsole('uploaded poster to node for video: ' + videoId, null, null);
}
else if (storageMode === 's3provider') {
const s3Config = storageConfig.s3Config;
const thumbnailImageKey = 'external/videos/' + videoId + '/images/thumbnail.jpg';
const previewImageKey = 'external/videos/' + videoId + '/images/preview.jpg';
const posterImageKey = 'external/videos/' + videoId + '/images/poster.jpg';
await s3_putObjectFromData(s3Config, thumbnailImageKey, thumbnailBuffer, 'image/jpeg');
await s3_putObjectFromData(s3Config, previewImageKey, previewFileBuffer, 'image/jpeg');
await s3_putObjectFromData(s3Config, posterImageKey, posterFileBuffer, 'image/jpeg');
}
await deleteDirectoryRecursive(imagesDirectoryPath);
await node_setVideoImported(jwtToken, videoId);
logDebugMessageToConsole('flagging video as imported to node for video: ' + videoId, null, null);
websocketClientBroadcast({ eventName: 'echo', jwtToken: jwtToken, data: { eventName: 'video_status', payload: { type: 'imported', videoId: videoId, lengthTimestamp: lengthTimestamp } } });
result = { isError: false };
}
else {
result = { isError: true, message: 'video file is missing' };
}
return result;
}
async function videoIdImportingStop_POST(jwtToken, videoId) {
websocketClientBroadcast({ eventName: 'echo', jwtToken: jwtToken, data: { eventName: 'video_status', payload: { type: 'importing_stopping', videoId: videoId } } });
const response = await node_stopVideoImporting(jwtToken, videoId);
if (!response.isError) {
websocketClientBroadcast({ eventName: 'echo', jwtToken: jwtToken, data: { eventName: 'video_status', payload: { type: 'importing_stopped', videoId: videoId } } });
}
return response;
}
async function videoIdPublishingStop_POST(jwtToken, videoId) {
websocketClientBroadcast({ eventName: 'echo', jwtToken: jwtToken, data: { eventName: 'video_status', payload: { type: 'publishing_stopping', videoId: videoId } } });
const response = await node_stopVideoPublishing(jwtToken, videoId);
if (!response.isError) {
websocketClientBroadcast({ eventName: 'echo', jwtToken: jwtToken, data: { eventName: 'video_status', payload: { type: 'publishing_stopped', videoId: videoId } } });
}
return response;
}
async function videoIdPublish_POST(jwtToken, videoId, publishings) {
let result;
publishings = JSON.parse(publishings);
const response1 = await node_getVideoData(videoId);
if (!response1.isError) {
const isLive = response1.videoData.isLive;
const isStreaming = response1.videoData.isStreaming;
const isFinalized = response1.videoData.isFinalized;
if (isLive && isStreaming) {
result = { isError: true, message: 'this video is currently streaming' };
}
else if (isFinalized) {
result = { isError: true, message: 'this video was finalized; no further publishings are possible' };
}
else {
const response2 = await node_getSourceFileExtension(jwtToken, videoId);
if (!response2.isError) {
const sourceFileExtension = response2.sourceFileExtension;
const sourceFilePath = path.join(getVideosDirectoryPath(), videoId + '/source/' + videoId + sourceFileExtension);
if (fs.existsSync(sourceFilePath)) {
for (const publishing of publishings) {
const format = publishing.format;
const resolution = publishing.resolution;
enqueuePendingPublishVideo({
jwtToken: jwtToken,
videoId: videoId,
format: format,
resolution: resolution,
sourceFileExtension: sourceFileExtension,
idleInterval: setInterval(function () {
websocketClientBroadcast({ eventName: 'echo', jwtToken: jwtToken, data: { eventName: 'video_status', payload: { type: 'publishing', videoId: videoId, format: format, resolution: resolution, progress: 0 } } });
}, 1000)
});
}
result = { isError: false };
}
else {
if (isLive) {
result = { isError: true, message: 'a recording of this stream does not exist<br>record your streams locally for later publishing' };
}
else {
result = { isError: true, message: 'a source for this video does not exist' };
}
}
}
else {
result = response2;
}
}
}
else {
result = response1;
}
return result;
}
async function videoIdUnpublish_POST(jwtToken, videoId, format, resolution) {
const nodeSettings = await getNodeSettings(jwtToken);
const storageConfig = nodeSettings.storageConfig;
await node_unpublishVideo(jwtToken, videoId, format, resolution);
if (storageConfig.storageMode === 's3provider') {
const s3Config = storageConfig.s3Config;
if (format === 'm3u8') {
const segmentsPrefix = 'external/videos/' + videoId + '/adaptive/m3u8/' + resolution;
const manifestKey = 'external/videos/' + videoId + '/adaptive/m3u8/static/manifests/manifest-' + resolution + '.m3u8';
await s3_deleteObjectsWithPrefix(s3Config, segmentsPrefix);
await s3_deleteObjectWithKey(s3Config, manifestKey);
}
else if (format === 'mp4' || format === 'webm' || format === 'ogv') {
const key = 'external/videos/' + videoId + '/progressive/' + format + '/' + resolution + '.' + format;
await s3_deleteObjectWithKey(s3Config, key);
}
}
if (format === 'm3u8') {
await refreshM3u8MasterManifest(jwtToken, videoId);
}
return { isError: false };
}
async function tags_GET(jwtToken) {
const response = await node_getVideosTags(jwtToken);
return response;
}
async function tagsAll_GET(jwtToken) {
const response = await node_getVideosTagsAll(jwtToken);
return response;
}
async function videoIdPublishes_GET(jwtToken, videoId) {
const response = await node_getVideoPublishes(jwtToken, videoId);
return response;
}
async function videoIdData_GET(videoId) {
const response = await node_getVideoData(videoId);
return response;
}
async function videoIdData_POST(jwtToken, videoId, title, description, tags) {
const response = await node_setVideoData(jwtToken, videoId, title, description, tags);
return response;
}
async function delete_POST(jwtToken, videoIds) {
const nodeResponseData = await node_deleteVideos(jwtToken, videoIds);
const deletedVideoIds = nodeResponseData.deletedVideoIds;
const nonDeletedVideoIds = nodeResponseData.nonDeletedVideoIds;
for (const deletedVideoId of deletedVideoIds) {
const deletedVideoIdPath = path.join(getVideosDirectoryPath(), deletedVideoId);
await deleteDirectoryRecursive(deletedVideoIdPath);
}
const nodeSettings = await getNodeSettings(jwtToken);
const storageConfig = nodeSettings.storageConfig;
if (storageConfig.storageMode === 's3provider') {
const s3Config = storageConfig.s3Config;
for (const videoId of videoIds) {
const videoPrefix = 'external/videos/' + videoId;
await s3_deleteObjectsWithPrefix(s3Config, videoPrefix);
}
}
return { isError: false, deletedVideoIds: deletedVideoIds, nonDeletedVideoIds: nonDeletedVideoIds };
}
async function finalize_POST(jwtToken, videoIds) {
let result;
const response = await node_finalizeVideos(jwtToken, videoIds);
if (!response.isError) {
const finalizedVideoIds = response.finalizedVideoIds;
const nonFinalizedVideoIds = response.nonFinalizedVideoIds;
for (const finalizedVideoId of finalizedVideoIds) {
const videoDirectory = path.join(getVideosDirectoryPath(), finalizedVideoId);
await deleteDirectoryRecursive(videoDirectory);
websocketClientBroadcast({ eventName: 'echo', jwtToken: jwtToken, data: { eventName: 'video_status', payload: { type: 'finalized', videoId: finalizedVideoId } } });
}
result = { isError: false, finalizedVideoIds: finalizedVideoIds, nonFinalizedVideoIds: nonFinalizedVideoIds };
}
else {
result = response;
}
return result;
}
async function videoIdIndexAdd_POST(jwtToken, videoId, containsAdultContent, termsOfServiceAgreed, cloudflareTurnstileToken) {
const response = await node_addVideoToIndex(jwtToken, videoId, containsAdultContent, termsOfServiceAgreed, cloudflareTurnstileToken);
return response;
}
async function videoIdIndexRemove_POST(jwtToken, videoId, cloudflareTurnstileToken) {
const response = await node_removeVideoFromIndex(jwtToken, videoId, cloudflareTurnstileToken);
return response;
}
async function videoIdThumbnail_POST(jwtToken, videoId, thumbnailFile) {
let result;
if (thumbnailFile != null && thumbnailFile.length === 1) {
thumbnailFile = thumbnailFile[0];
const thumbnailBuffer = await sharp(thumbnailFile.buffer).resize({ width: 100 }).resize(100, 100).jpeg({ quality: 90 }).toBuffer();
const nodeSettings = await getNodeSettings(jwtToken);
const storageConfig = nodeSettings.storageConfig;
const storageMode = storageConfig.storageMode;
if (storageMode === 'filesystem') {
logDebugMessageToConsole('uploading thumbnail image to node for video: ' + videoId, null, null);
await node_setThumbnail(jwtToken, videoId, thumbnailBuffer);
logDebugMessageToConsole('uploaded thumbnail image to node for video: ' + videoId, null, null);
}
else if (storageMode === 's3provider') {
logDebugMessageToConsole('uploading thumbnail image to s3 for video: ' + videoId, null, null);
const s3Config = storageConfig.s3Config;
const key = 'external/videos/' + videoId + '/images/thumbnail.jpg';
await s3_putObjectFromData(s3Config, key, thumbnailBuffer, 'image/jpeg');
await node_setIsIndexOutdated(jwtToken, videoId);
logDebugMessageToConsole('uploaded thumbnail image to s3 for video: ' + videoId, null, null);
}
else {
throw new Error('videoIdThumbnail_POST received invalid storageMode: ' + storageMode);
}
result = { isError: false };
}
else {
result = { isError: true, message: 'thumbnail file is missing' };
}
return result;
}
async function videoIdPreview_POST(jwtToken, videoId, previewFile) {
let result;
if (previewFile != null && previewFile.length === 1) {
previewFile = previewFile[0];
const previewFileBuffer = await sharp(previewFile.buffer).resize({ width: 512 }).resize(512, 288).jpeg({ quality: 90 }).toBuffer();
const nodeSettings = await getNodeSettings(jwtToken);
const storageConfig = nodeSettings.storageConfig;
const storageMode = storageConfig.storageMode;
if (storageMode === 'filesystem') {
logDebugMessageToConsole('uploading preview image to node for video: ' + videoId, null, null);
await node_setPreview(jwtToken, videoId, previewFileBuffer);
logDebugMessageToConsole('uploaded preview image to node for video: ' + videoId, null, null);
}
else if (storageMode === 's3provider') {
logDebugMessageToConsole('uploading preview image to s3 for video: ' + videoId, null, null);
const s3Config = storageConfig.s3Config;
const key = 'external/videos/' + videoId + '/images/preview.jpg';
await s3_putObjectFromData(s3Config, key, previewFileBuffer, 'image/jpeg');
await node_setIsIndexOutdated(jwtToken, videoId);
logDebugMessageToConsole('uploaded preview image to s3 for video: ' + videoId, null, null);
}
else {
throw new Error('videoIdPreview_POST received invalid storageMode: ' + storageMode);
}
result = { isError: false };
}
else {
result = { isError: true, message: 'preview file is missing' };
}
return result;
}
async function videoIdPoster_POST(jwtToken, videoId, posterFile) {
let result;
if (posterFile != null && posterFile.length === 1) {
posterFile = posterFile[0];
const posterFileBuffer = await sharp(posterFile.buffer).resize({ width: 1280 }).resize(1280, 720).jpeg({ quality: 90 }).toBuffer();
const nodeSettings = await getNodeSettings(jwtToken);
const storageConfig = nodeSettings.storageConfig;
const storageMode = storageConfig.storageMode;
if (storageMode === 'filesystem') {
logDebugMessageToConsole('uploading poster image to node for video: ' + videoId, null, null);
await node_setPoster(jwtToken, videoId, posterFileBuffer);
logDebugMessageToConsole('uploaded poster image to node for video: ' + videoId, null, null);
}
else if (storageMode === 's3provider') {
logDebugMessageToConsole('uploading poster image to s3 for video: ' + videoId, null, null);
const s3Config = storageConfig.s3Config;
const key = 'external/videos/' + videoId + '/images/poster.jpg';
await s3_putObjectFromData(s3Config, key, posterFileBuffer, 'image/jpeg');
await node_setIsIndexOutdated(jwtToken, videoId);
logDebugMessageToConsole('uploaded poster image to s3 for video: ' + videoId, null, null);
}
else {
throw new Error('videoIdPoster_POST received invalid storageMode: ' + storageMode);
}
result = { isError: false };
}
else {
result = { isError: true, message: 'poster file is missing' };
}
return result;
}
async function videoIdSources_GET(videoId) {
let result;
const response = await node_getVideoSources(videoId);
if (!response.isError) {
const video = response.video;
const adaptiveSources = video.adaptiveSources;
const progressiveSources = video.progressiveSources;
result = { isError: false, sources: { adaptiveSources: adaptiveSources, progressiveSources: progressiveSources } };
}
else {
result = response;
}
return result;
}
async function videoIdPermissions_GET(jwtToken, videoId) {
const response = await node_getVideoPermissions(jwtToken, videoId);
return response;
}
async function videoIdPermissions_POST(jwtToken, videoId, type, isEnabled) {
const response = await node_postVideoPermissions(jwtToken, videoId, type, isEnabled);
return response;
}
module.exports = {
search_GET,
import_POST,
videoIdImportingStop_POST,
videoIdPublishingStop_POST,
videoIdPublish_POST,
videoIdUnpublish_POST,
tags_GET,
tagsAll_GET,
videoIdPublishes_GET,
videoIdData_GET,
videoIdData_POST,
delete_POST,
finalize_POST,
videoIdIndexAdd_POST,
videoIdIndexRemove_POST,
videoIdThumbnail_POST,
videoIdPreview_POST,
videoIdPoster_POST,
videoIdSources_GET,
videoIdPermissions_GET,
videoIdPermissions_POST
};