Skip to content

Commit

Permalink
Merge pull request #2174 from dennisrijsdijk/media-metadata
Browse files Browse the repository at this point in the history
Media metadata
  • Loading branch information
itsjesski committed Sep 15, 2023
2 parents 08324ca + 3a3686e commit f87e7ed
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 78 deletions.
86 changes: 27 additions & 59 deletions src/backend/effects/builtin/play-video.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ const accountAccess = require("../../common/account-access");
const util = require("../../utility");
const fs = require('fs-extra');
const path = require("path");
const frontendCommunicator = require('../../common/frontend-communicator');
const { wait } = require("../../utility");
const { parseYoutubeId } = require("../../../shared/youtube-url-parser");

/**
* The Play Video effect
Expand Down Expand Up @@ -439,40 +442,31 @@ const playVideo = {
}

let resourceToken;
let duration;
if (effect.videoType === "YouTube Video") {
resourceToken = resourceTokenManager.storeResourcePath(data.filepath, effect.length);
const youtubeData = parseYoutubeId(data.youtubeId);
data.youtubeId = youtubeData.id;
const result = await frontendCommunicator.fireEventAsync("getYoutubeVideoDuration", data.youtubeId);
if (!isNaN(result)) {
duration = result;
}
if (data.videoStarttime == null || data.videoStarttime == "" || data.videoStarttime == 0) {
data.videoStarttime = youtubeData.startTime;
}
} else {
const durationToken = resourceTokenManager.storeResourcePath(data.filepath, 5);

const durationPromise = new Promise(async (resolve, reject) => {
const listener = (event) => {
try {
if (event.name === "video-duration" && event.data.resourceToken === durationToken) {
webServer.removeListener("overlay-event", listener);
resolve(event.data.duration);
}
} catch (err) {
logger.error("Error while trying to process overlay-event for getVideoDuration: ", err);
reject(err);
}
};
webServer.on("overlay-event", listener);
});

webServer.sendToOverlay("getVideoDuration", {
resourceToken: durationToken,
overlayInstance: data.overlayInstance
});

const duration = await durationPromise;
resourceToken = resourceTokenManager.storeResourcePath(data.filepath, duration + 5);
logger.info(`Retrieved duration for video: ${duration}`);
const result = await frontendCommunicator.fireEventAsync("getVideoDuration", data.filepath);
if (!isNaN(result)) {
duration = result;
}
resourceToken = resourceTokenManager.storeResourcePath(data.filepath, duration);
}

data.resourceToken = resourceToken;

webServer.sendToOverlay("video", data);
if (effect.wait) {
if (effect.wait && effect.videoType === "YouTube Video") {
// Use overlay callback for youtube video, needs local way to get duration for production.
// If effect is ran with "Wait for video to finish" while overlay is not open, it may freeze an effect queue.
await new Promise(async (resolve, reject) => {
const listener = (event) => {
try {
Expand All @@ -487,6 +481,12 @@ const playVideo = {
};
webServer.on("overlay-event", listener);
});
} else if (effect.wait && data.videoType === "Local Video") {
let internalDuration = data.videoDuration;
if (internalDuration == null || internalDuration === 0 || internalDuration === "") {
internalDuration = duration;
}
await wait(internalDuration * 1000);
}
return true;
},
Expand Down Expand Up @@ -637,9 +637,6 @@ const playVideo = {
const exitVideo = () => {
delete startedVidCache[this.id]; // eslint-disable-line no-undef
animateVideoExit(`#${wrapperId}`, exitAnimation, exitDuration, inbetweenAnimation);
setTimeout(function(){
sendWebsocketEvent("video-end", {resourceToken: token}); // eslint-disable-line no-undef
}, millisecondsFromString(exitDuration));
};

// Remove div after X time.
Expand Down Expand Up @@ -669,29 +666,6 @@ const playVideo = {

$(".wrapper").append(wrappedHtml);

try {
const url = new URL(youtubeId);
if (url.hostname.includes("www.youtube.com")) {
for (const [key, value] of url.searchParams) {
if (key === "v") {
youtubeId = value;
} else if (key === "t") {
videoStarttime = value;
}
}
}
if (url.hostname.includes("youtu.be")) {
youtubeId = url.pathname.replace("/", "");
for (const [key, value] of url.searchParams) {
if (key === "t") {
videoStarttime = value;
}
}
}
} catch (error) {
//failed to convert url
}

// Add iframe.

const playerVars = {
Expand Down Expand Up @@ -737,9 +711,6 @@ const playVideo = {
onStateChange: (event) => {
if (event.data === 0 && !videoDuration) {
animateVideoExit(`#${wrapperId}`, exitAnimation, exitDuration, inbetweenAnimation);
setTimeout(function(){
sendWebsocketEvent("video-end", {resourceToken: token}); // eslint-disable-line no-undef
}, millisecondsFromString(exitDuration));
}
}
}
Expand All @@ -757,9 +728,6 @@ const playVideo = {
if (videoDuration) {
setTimeout(function () {
animateVideoExit(`#${wrapperId}`, exitAnimation, exitDuration, inbetweenAnimation);
setTimeout(function(){
sendWebsocketEvent("video-end", {resourceToken: token}); // eslint-disable-line no-undef
}, millisecondsFromString(exitDuration));
}, videoDuration);
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/backend/variables/builtin-variable-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ exports.loadReplaceVariables = () => {
'array-reverse',
'array-shuffle-raw',
'array-shuffle',
'audio-duration',
'bits-badge-tier',
'bits-badge-unlocked-message',
'bits-cheered',
Expand Down Expand Up @@ -203,9 +204,11 @@ exports.loadReplaceVariables = () => {
'username-array-raw',
'username-array',
'username',
'video-duration',
'view-time',
'whisper-message',
'word'
'word',
'youtube-video-duration'
].forEach(filename => {
const definition = require(`./builtin/${filename}`);
replaceVariableManager.registerReplaceVariable(definition);
Expand Down
29 changes: 29 additions & 0 deletions src/backend/variables/builtin/audio-duration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"use strict";

const { OutputDataType, VariableCategory } = require("../../../shared/variable-constants");

const frontendCommunicator = require("../../common/frontend-communicator");

const model = {
definition: {
handle: "audioDuration",
usage: "audioDuration[filePathOrUrl]",
description: "Attempts to retrieve audio duration.",
categories: [VariableCategory.ADVANCED],
possibleDataOutput: [OutputDataType.NUMBER]
},
evaluator: async (trigger, url) => {
if (url == null) {
return "[NO URL PROVIDED]";
}
try {
return await frontendCommunicator.fireEventAsync("getSoundDuration", {
path: url
});
} catch (err) {
return "[ERROR FETCHING DURATION]";
}
}
};

module.exports = model;
30 changes: 30 additions & 0 deletions src/backend/variables/builtin/video-duration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"use strict";

const { OutputDataType, VariableCategory } = require("../../../shared/variable-constants");

const frontendCommunicator = require("../../common/frontend-communicator");
const logger = require("../../logwrapper");

const model = {
definition: {
handle: "videoDuration",
usage: "videoDuration[filePathOrUrl]",
description: "Attempts to retrieve video duration.",
categories: [VariableCategory.ADVANCED],
possibleDataOutput: [OutputDataType.TEXT]
},
evaluator: async (trigger, url) => {
if (url == null) {
return "[NO URL PROVIDED]";
}
const result = await frontendCommunicator.fireEventAsync("getVideoDuration", url);

if (isNaN(result)) {
logger.error("Error while retrieving video duration", result);
return "[ERROR FETCHING DURATION]";
}
return result;
}
};

module.exports = model;
30 changes: 30 additions & 0 deletions src/backend/variables/builtin/youtube-video-duration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"use strict";

const { OutputDataType, VariableCategory } = require("../../../shared/variable-constants");
const logger = require("../../logwrapper");
const frontendCommunicator = require("../../common/frontend-communicator");
const { parseYoutubeId } = require("../../../shared/youtube-url-parser");

const model = {
definition: {
handle: "youtubeVideoDuration",
usage: "youtubeVideoDuration[urlOrId]",
description: "Attempts to retrieve youtube video duration.",
categories: [VariableCategory.ADVANCED],
possibleDataOutput: [OutputDataType.TEXT]
},
evaluator: async (trigger, id) => {
if (id == null) {
return "[NO VIDEO ID PROVIDED]";
}
const result = await frontendCommunicator.fireEventAsync("getYoutubeVideoDuration", parseYoutubeId(id).id);

if (isNaN(result)) {
logger.error("Error while retrieving youtube video duration", result);
return "[ERROR FETCHING DURATION]";
}
return result;
}
};

module.exports = model;
5 changes: 3 additions & 2 deletions src/gui/app/app-main.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,10 @@
scheduledTaskService,
channelRewardsService,
sortTagsService,
iconsService
iconsService,
videoService
) {
// 'chatMessagesService' is included so its instantiated on app start
// 'chatMessagesService' and 'videoService' are included so they're instantiated on app start

connectionService.loadProfiles();

Expand Down
2 changes: 2 additions & 0 deletions src/gui/app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@

<script src="./app-main.js"></script>

<script src="https://www.youtube.com/iframe_api"></script>

<!-- include: "type": "js", "files": "controllers/**/*.js" -->
<script type="text/javascript" src="./controllers/channel-rewards.controller.js"></script>
<script type="text/javascript" src="./controllers/chat-messages.controller.js"></script>
Expand Down
87 changes: 87 additions & 0 deletions src/gui/app/services/video.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"use strict";

(function() {

const uuid = require("uuid");

// This provides methods for playing sounds

angular
.module("firebotApp")
.factory("videoService", function(backendCommunicator) {
const service = {};

service.getVideoDuration = function (path) {
return new Promise((resolve) => {
const id = "video-" + uuid();
const videoElement = `<video id="${id}" preload="metadata" style="display: none;"></video>`;
$(document.documentElement).append(videoElement);
const video = document.getElementById(id);
video.onloadedmetadata = () => {
resolve(video.duration);
video.remove();
};

video.onerror = () => {
const result = {
error: video.error.message,
path: video.src
};
video.remove();
resolve(result);
};

video.src = path;
});
};

service.getYoutubeVideoDuration = function (videoId) {
return new Promise((resolve) => {
const id = "video-" + uuid();
$(document.documentElement).append(`<div id="${id}" style="display: none;"></div>`);
// eslint-disable-next-line no-undef
const player = new YT.Player(id, {
videoId: videoId,
events: {
onReady: (event) => {
event.target.setVolume(0);
event.target.playVideo();
resolve(player.getDuration());
document.getElementById(id).remove();
},
onError: (event) => {
const error = {
code: event.data,
youtubeId: videoId
};
if (event.data === 2) {
error.error = "The request contains an invalid parameter value.";
}
if (event.data === 5) {
error.error = "The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.";
}
if (event.data === 100) {
error.error = "The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.";
}
if (event.data === 101 || event.data === 150) {
error.error = "The owner of the requested video does not allow it to be played in embedded players.";
}
resolve(error);
document.getElementById(id).remove();
}
}
});
});
};

backendCommunicator.onAsync("getVideoDuration", async (path) => {
return service.getVideoDuration(path);
});

backendCommunicator.onAsync("getYoutubeVideoDuration", async (youtubeId) => {
return service.getYoutubeVideoDuration(youtubeId);
});

return service;
});
}(window.angular));
16 changes: 0 additions & 16 deletions src/resources/overlay/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,22 +49,6 @@ function overlaySocketConnect(){
return;
}

if(event == "getVideoDuration") {
const token = encodeURIComponent(data.meta.resourceToken);
const url = `http://${window.location.hostname}:7472/resource/${token}`;
const videoElement = `
<video id="${token}" class="player" style="display:none;">
<source src="${url}">
</video>
`;
$(".wrapper").append(videoElement);
const video = document.getElementById(token);
video.oncanplay = () => {
sendWebsocketEvent("video-duration", {resourceToken: token, duration: Math.ceil(video.duration)});
video.remove();
}
}

firebotOverlay.emit(event, data.meta);
};

Expand Down
Loading

0 comments on commit f87e7ed

Please sign in to comment.