From a63a35c74d916c77b80aa6441aa0d9e2e12fd465 Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 17 Jan 2024 11:38:51 +0600 Subject: [PATCH] twitter: add option to convert .mp4 to .gif --- src/config.json | 3 +- src/front/cobalt.js | 2 ++ src/localization/languages/en.json | 4 ++- src/modules/pageRender/page.js | 10 ++++++ src/modules/processing/match.js | 9 +++-- src/modules/processing/matchActionDecider.js | 9 +++-- src/modules/processing/services/twitter.js | 9 +++-- src/modules/stream/stream.js | 5 ++- src/modules/stream/types.js | 37 ++++++++++++++++++++ src/modules/sub/utils.js | 5 +-- 10 files changed, 81 insertions(+), 12 deletions(-) diff --git a/src/config.json b/src/config.json index 91922a5e..923f4d7b 100644 --- a/src/config.json +++ b/src/config.json @@ -90,7 +90,8 @@ "mp4": ["-c:v", "copy", "-c:a", "copy", "-movflags", "faststart+frag_keyframe+empty_moov"], "copy": ["-c:a", "copy"], "audio": ["-ar", "48000", "-ac", "2", "-b:a", "320k"], - "m4a": ["-movflags", "frag_keyframe+empty_moov"] + "m4a": ["-movflags", "frag_keyframe+empty_moov"], + "gif": ["-vf", "scale=-1:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse", "-loop", "0"] }, "sponsors": [{ "name": "royale", diff --git a/src/front/cobalt.js b/src/front/cobalt.js index 31196215..de45aa3a 100644 --- a/src/front/cobalt.js +++ b/src/front/cobalt.js @@ -30,6 +30,7 @@ const checkboxes = [ "reduceTransparency", "disableAnimations", "disableMetadata", + "twitterGif", ]; const exceptions = { // used for mobile devices "vQuality": "720" @@ -381,6 +382,7 @@ async function download(url) { } if (sGet("disableMetadata") === "true") req.disableMetadata = true; + if (sGet("twitterGif") === "true") req.twitterGif = true; let j = await fetch(`${apiURL}/api/json`, { method: "POST", diff --git a/src/localization/languages/en.json b/src/localization/languages/en.json index 70161ef0..71f55117 100644 --- a/src/localization/languages/en.json +++ b/src/localization/languages/en.json @@ -158,6 +158,8 @@ "UrgentTwitterPatch": "fixes and easier downloads", "StatusPage": "service status page", "TroubleshootingGuide": "self-troubleshooting guide", - "UpdateNewYears": "new years clean up" + "UpdateNewYears": "new years clean up", + "SettingsTwitterGif": "convert gifs to .gif", + "SettingsTwitterGifDescription": ".gif is lossy and extremely inefficient. file sizes may be larger than expected. use only when necessary." } } diff --git a/src/modules/pageRender/page.js b/src/modules/pageRender/page.js index c3722f0d..affc4fbb 100644 --- a/src/modules/pageRender/page.js +++ b/src/modules/pageRender/page.js @@ -327,6 +327,16 @@ export default function(obj) { padding: "no-margin" }]) }) + + settingsCategory({ + name: "twitter", + title: "twitter", + body: checkbox([{ + action: "twitterGif", + name: t("SettingsTwitterGif"), + padding: "no-margin" + }]) + + explanation(t('SettingsTwitterGifDescription')) + }) + settingsCategory({ name: "codec", title: t('SettingsCodecSubtitle'), diff --git a/src/modules/processing/match.js b/src/modules/processing/match.js index 12c0bc10..267fc258 100644 --- a/src/modules/processing/match.js +++ b/src/modules/processing/match.js @@ -37,7 +37,8 @@ export default async function(host, patternMatch, url, lang, obj) { case "twitter": r = await twitter({ id: patternMatch.id, - index: patternMatch.index - 1 + index: patternMatch.index - 1, + toGif: obj.twitterGif }); break; case "vk": @@ -166,7 +167,11 @@ export default async function(host, patternMatch, url, lang, obj) { : loc(lang, r.error) }) - return matchActionDecider(r, host, obj.aFormat, isAudioOnly, lang, isAudioMuted, disableMetadata, obj.filenamePattern) + return matchActionDecider( + r, host, obj.aFormat, isAudioOnly, + lang, isAudioMuted, disableMetadata, + obj.filenamePattern, obj.twitterGif + ) } catch (e) { return apiJSON(0, { t: genericError(lang, host) }) } diff --git a/src/modules/processing/matchActionDecider.js b/src/modules/processing/matchActionDecider.js index a7b8740b..f015ea13 100644 --- a/src/modules/processing/matchActionDecider.js +++ b/src/modules/processing/matchActionDecider.js @@ -3,7 +3,7 @@ import { apiJSON } from "../sub/utils.js"; import loc from "../../localization/manager.js"; import createFilename from "./createFilename.js"; -export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, disableMetadata, filenamePattern) { +export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, disableMetadata, filenamePattern, toGif) { let action, responseType = 2, defaultParams = { @@ -14,13 +14,14 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di fileMetadata: !disableMetadata ? r.fileMetadata : false }, params = {}, - audioFormat = String(userFormat) + audioFormat = String(userFormat); if (r.isPhoto) action = "photo"; else if (r.picker) action = "picker" else if (isAudioMuted) action = "muteVideo"; else if (isAudioOnly) action = "audio"; else if (r.isM3U8) action = "singleM3U8"; + else if (r.isGif && toGif) action = "gif"; else action = "video"; if (action === "picker" || action === "audio") { @@ -39,6 +40,10 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di case "photo": responseType = 1; break; + + case "gif": + params = { type: "gif" } + break; case "singleM3U8": params = { type: "remux" } diff --git a/src/modules/processing/services/twitter.js b/src/modules/processing/services/twitter.js index 31b26b48..ee6a9f12 100644 --- a/src/modules/processing/services/twitter.js +++ b/src/modules/processing/services/twitter.js @@ -72,7 +72,7 @@ const requestTweet = (tweetId, token) => { }) } -export default async function({ id, index }) { +export default async function({ id, index, toGif }) { let guestToken = await getGuestToken(); if (!guestToken) return { error: 'ErrorCouldntFetch' }; @@ -110,7 +110,8 @@ export default async function({ id, index }) { type: needsFixing(media[0]) ? "remux" : "normal", urls: bestQuality(media[0].video_info.variants), filename: `twitter_${id}.mp4`, - audioFilename: `twitter_${id}_audio` + audioFilename: `twitter_${id}_audio`, + isGif: media[0].type === "animated_gif" }; default: const picker = media.map((video, i) => { @@ -120,7 +121,9 @@ export default async function({ id, index }) { service: 'twitter', type: 'remux', u: url, - filename: `twitter_${id}_${i + 1}.mp4` + filename: `twitter_${id}_${i + 1}.mp4`, + isGif: media[0].type === "animated_gif", + toGif: toGif ?? false }) } return { diff --git a/src/modules/stream/stream.js b/src/modules/stream/stream.js index fb4f7ed6..f254dacc 100644 --- a/src/modules/stream/stream.js +++ b/src/modules/stream/stream.js @@ -1,4 +1,4 @@ -import { streamAudioOnly, streamDefault, streamLiveRender, streamVideoOnly } from "./types.js"; +import { streamAudioOnly, streamDefault, streamLiveRender, streamVideoOnly, convertToGif } from "./types.js"; export default async function(res, streamInfo) { try { @@ -10,6 +10,9 @@ export default async function(res, streamInfo) { case "render": await streamLiveRender(streamInfo, res); break; + case "gif": + convertToGif(streamInfo, res); + break; case "remux": case "mute": streamVideoOnly(streamInfo, res); diff --git a/src/modules/stream/types.js b/src/modules/stream/types.js index c2bd2910..88ebc61e 100644 --- a/src/modules/stream/types.js +++ b/src/modules/stream/types.js @@ -212,3 +212,40 @@ export function streamVideoOnly(streamInfo, res) { shutdown(); } } + +export function convertToGif(streamInfo, res) { + let process; + const shutdown = () => (killProcess(process), closeResponse(res)); + + try { + let args = [ + '-loglevel', '-8' + ] + if (streamInfo.service === "twitter") { + args.push('-seekable', '0') + } + args.push('-i', streamInfo.urls) + args = args.concat(ffmpegArgs["gif"]); + args.push('-f', "gif", 'pipe:3'); + + process = spawn(ffmpeg, args, { + windowsHide: true, + stdio: [ + 'inherit', 'inherit', 'inherit', + 'pipe' + ], + }); + + const [,,, muxOutput] = process.stdio; + + res.setHeader('Connection', 'keep-alive'); + res.setHeader('Content-Disposition', contentDisposition(streamInfo.filename.split('.')[0] + ".gif")); + + pipe(muxOutput, res, shutdown); + + process.on('close', shutdown); + res.on('finish', shutdown); + } catch { + shutdown(); + } +} diff --git a/src/modules/sub/utils.js b/src/modules/sub/utils.js index 28d37c6c..8b5a2c84 100644 --- a/src/modules/sub/utils.js +++ b/src/modules/sub/utils.js @@ -8,7 +8,7 @@ const apiVar = { aFormat: ["best", "mp3", "ogg", "wav", "opus"], filenamePattern: ["classic", "pretty", "basic", "nerdy"] }, - booleanOnly: ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio", "isAudioMuted", "dubLang", "vimeoDash", "disableMetadata"] + booleanOnly: ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio", "isAudioMuted", "dubLang", "vimeoDash", "disableMetadata", "twitterGif"] } const forbiddenChars = ['}', '{', '(', ')', '\\', '>', '<', '^', '*', '!', '~', ';', ':', ',', '`', '[', ']', '#', '$', '"', "'", "@", '==']; const forbiddenCharsString = ['}', '{', '%', '>', '<', '^', ';', '`', '$', '"', "@", '=']; @@ -84,7 +84,8 @@ export function checkJSONPost(obj) { isAudioMuted: false, disableMetadata: false, dubLang: false, - vimeoDash: false + vimeoDash: false, + twitterGif: false } try { let objKeys = Object.keys(obj);