From d63326651c76d5d7cd49067e37f2ce0a0ef5bebb Mon Sep 17 00:00:00 2001 From: Lexie Date: Mon, 30 Sep 2024 10:41:01 +0200 Subject: [PATCH] fix video embeds (#12) --- README.md | 5 + pkgs/api/Dockerfile | 2 + pkgs/api/package.json | 5 +- pkgs/api/pnpm-lock.yaml | 181 ---------------------- pkgs/api/src/index.ts | 51 +++--- pkgs/api/src/utils.ts | 93 +++++------ pkgs/app/src/components/Post.tsx | 46 ++++-- pkgs/app/src/lib/parseEmbedDescription.ts | 5 +- pkgs/app/src/lib/processVideoEmbed.ts | 5 + pkgs/app/src/lib/utils.ts | 8 +- pkgs/app/src/routes/getOEmbed.ts | 5 +- pkgs/app/src/routes/getPost.tsx | 8 +- 12 files changed, 135 insertions(+), 279 deletions(-) diff --git a/README.md b/README.md index 4e093ea..bf6589f 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,11 @@ Embed Bluesky links in Discord. #### Simply append `x` at the end of `bsky.app`. +## FAQ + +### [Video is too long to embed] in an embed description. +Due to Discord's and BlueSky limitations, the video cannot be embedded if it exceeds ~30s. You can still view the video by clicking on the `VixBluesky` in the author field. + ## Authors - [@ItsRauf](https://www.github.com/ItsRauf) - Original author diff --git a/pkgs/api/Dockerfile b/pkgs/api/Dockerfile index 292190d..49ebf20 100644 --- a/pkgs/api/Dockerfile +++ b/pkgs/api/Dockerfile @@ -2,6 +2,8 @@ FROM node:22-alpine WORKDIR /app +RUN apk add ffmpeg + COPY package*.json ./ RUN npm i -g pnpm && pnpm install diff --git a/pkgs/api/package.json b/pkgs/api/package.json index d14f490..95ec814 100644 --- a/pkgs/api/package.json +++ b/pkgs/api/package.json @@ -11,12 +11,9 @@ "author": "Lexedia", "license": "ISC", "dependencies": { - "fastify": "^4.28.1", - "ffmpeg-static": "^5.2.0", - "fluent-ffmpeg": "^2.1.3" + "fastify": "^4.28.1" }, "devDependencies": { - "@types/fluent-ffmpeg": "^2.1.26", "@types/node": "^22.5.4", "pino-pretty": "^11.2.2", "typescript": "^5.6.2" diff --git a/pkgs/api/pnpm-lock.yaml b/pkgs/api/pnpm-lock.yaml index b217e14..435466a 100644 --- a/pkgs/api/pnpm-lock.yaml +++ b/pkgs/api/pnpm-lock.yaml @@ -11,16 +11,7 @@ importers: fastify: specifier: ^4.28.1 version: 4.28.1 - ffmpeg-static: - specifier: ^5.2.0 - version: 5.2.0 - fluent-ffmpeg: - specifier: ^2.1.3 - version: 2.1.3 devDependencies: - '@types/fluent-ffmpeg': - specifier: ^2.1.26 - version: 2.1.26 '@types/node': specifier: ^22.5.4 version: 22.5.4 @@ -33,10 +24,6 @@ importers: packages: - '@derhuerst/http-basic@8.2.4': - resolution: {integrity: sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==} - engines: {node: '>=6.0.0'} - '@fastify/ajv-compiler@3.6.0': resolution: {integrity: sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==} @@ -49,12 +36,6 @@ packages: '@fastify/merge-json-schemas@0.1.1': resolution: {integrity: sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==} - '@types/fluent-ffmpeg@2.1.26': - resolution: {integrity: sha512-0JVF3wdQG+pN0ImwWD0bNgJiKF2OHg/7CDBHw5UIbRTvlnkgGHK6V5doE54ltvhud4o31/dEiHm23CAlxFiUQg==} - - '@types/node@10.17.60': - resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==} - '@types/node@22.5.4': resolution: {integrity: sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==} @@ -65,10 +46,6 @@ packages: abstract-logging@2.0.1: resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} - agent-base@6.0.2: - resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} - engines: {node: '>= 6.0.0'} - ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -88,9 +65,6 @@ packages: ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} - async@0.2.10: - resolution: {integrity: sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==} - atomic-sleep@1.0.0: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} @@ -101,22 +75,12 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - caseless@0.12.0: - resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} - colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} - concat-stream@2.0.0: - resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} - engines: {'0': node >= 6.0} - cookie@0.6.0: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} @@ -124,22 +88,9 @@ packages: dateformat@4.6.3: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} - debug@4.3.7: - resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} - env-paths@2.2.1: - resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} - engines: {node: '>=6'} - event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} @@ -185,18 +136,10 @@ packages: fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} - ffmpeg-static@5.2.0: - resolution: {integrity: sha512-WrM7kLW+do9HLr+H6tk7LzQ7kPqbAgLjdzNE32+u3Ff11gXt9Kkkd2nusGFrlWMIe+XaA97t+I8JS7sZIrvRgA==} - engines: {node: '>=16'} - find-my-way@8.2.0: resolution: {integrity: sha512-HdWXgFYc6b1BJcOBDBwjqWuHJj1WYiqrxSh25qtU4DabpMFdj/gSunNBQb83t+8Zt67D7CXEzJWTkxaShMTMOA==} engines: {node: '>=14'} - fluent-ffmpeg@2.1.3: - resolution: {integrity: sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==} - engines: {node: '>=18'} - forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -204,26 +147,13 @@ packages: help-me@5.0.0: resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} - http-response-object@3.0.2: - resolution: {integrity: sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==} - - https-proxy-agent@5.0.1: - resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} - engines: {node: '>= 6'} - ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -240,9 +170,6 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} @@ -250,9 +177,6 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - parse-cache-control@1.0.1: - resolution: {integrity: sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==} - pino-abstract-transport@1.2.0: resolution: {integrity: sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==} @@ -277,10 +201,6 @@ packages: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} - progress@2.0.3: - resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} - engines: {node: '>=0.4.0'} - proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -291,10 +211,6 @@ packages: quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} - readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - readable-stream@4.5.2: resolution: {integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -360,9 +276,6 @@ packages: resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} engines: {node: '>=12'} - typedarray@0.0.6: - resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typescript@5.6.2: resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==} engines: {node: '>=14.17'} @@ -371,25 +284,11 @@ packages: undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - which@1.3.1: - resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} - hasBin: true - wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} snapshots: - '@derhuerst/http-basic@8.2.4': - dependencies: - caseless: 0.12.0 - concat-stream: 2.0.0 - http-response-object: 3.0.2 - parse-cache-control: 1.0.1 - '@fastify/ajv-compiler@3.6.0': dependencies: ajv: 8.17.1 @@ -406,12 +305,6 @@ snapshots: dependencies: fast-deep-equal: 3.1.3 - '@types/fluent-ffmpeg@2.1.26': - dependencies: - '@types/node': 22.5.4 - - '@types/node@10.17.60': {} - '@types/node@22.5.4': dependencies: undici-types: 6.19.8 @@ -422,12 +315,6 @@ snapshots: abstract-logging@2.0.1: {} - agent-base@6.0.2: - dependencies: - debug: 4.3.7 - transitivePeerDependencies: - - supports-color - ajv-formats@2.1.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -443,8 +330,6 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - async@0.2.10: {} - atomic-sleep@1.0.0: {} avvio@8.4.0: @@ -454,38 +339,21 @@ snapshots: base64-js@1.5.1: {} - buffer-from@1.1.2: {} - buffer@6.0.3: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 - caseless@0.12.0: {} - colorette@2.0.20: {} - concat-stream@2.0.0: - dependencies: - buffer-from: 1.1.2 - inherits: 2.0.4 - readable-stream: 3.6.2 - typedarray: 0.0.6 - cookie@0.6.0: {} dateformat@4.6.3: {} - debug@4.3.7: - dependencies: - ms: 2.1.3 - end-of-stream@1.4.4: dependencies: once: 1.4.0 - env-paths@2.2.1: {} - event-target-shim@5.0.1: {} events@3.3.0: {} @@ -543,49 +411,20 @@ snapshots: dependencies: reusify: 1.0.4 - ffmpeg-static@5.2.0: - dependencies: - '@derhuerst/http-basic': 8.2.4 - env-paths: 2.2.1 - https-proxy-agent: 5.0.1 - progress: 2.0.3 - transitivePeerDependencies: - - supports-color - find-my-way@8.2.0: dependencies: fast-deep-equal: 3.1.3 fast-querystring: 1.1.2 safe-regex2: 3.1.0 - fluent-ffmpeg@2.1.3: - dependencies: - async: 0.2.10 - which: 1.3.1 - forwarded@0.2.0: {} help-me@5.0.0: {} - http-response-object@3.0.2: - dependencies: - '@types/node': 10.17.60 - - https-proxy-agent@5.0.1: - dependencies: - agent-base: 6.0.2 - debug: 4.3.7 - transitivePeerDependencies: - - supports-color - ieee754@1.2.1: {} - inherits@2.0.4: {} - ipaddr.js@1.9.1: {} - isexe@2.0.0: {} - joycon@3.1.1: {} json-schema-ref-resolver@1.0.1: @@ -602,16 +441,12 @@ snapshots: minimist@1.2.8: {} - ms@2.1.3: {} - on-exit-leak-free@2.1.2: {} once@1.4.0: dependencies: wrappy: 1.0.2 - parse-cache-control@1.0.1: {} - pino-abstract-transport@1.2.0: dependencies: readable-stream: 4.5.2 @@ -656,8 +491,6 @@ snapshots: process@0.11.10: {} - progress@2.0.3: {} - proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -670,12 +503,6 @@ snapshots: quick-format-unescaped@4.0.4: {} - readable-stream@3.6.2: - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - readable-stream@4.5.2: dependencies: abort-controller: 3.0.0 @@ -726,16 +553,8 @@ snapshots: toad-cache@3.7.0: {} - typedarray@0.0.6: {} - typescript@5.6.2: {} undici-types@6.19.8: {} - util-deprecate@1.0.2: {} - - which@1.3.1: - dependencies: - isexe: 2.0.0 - wrappy@1.0.2: {} diff --git a/pkgs/api/src/index.ts b/pkgs/api/src/index.ts index 9330b96..f5d66dd 100644 --- a/pkgs/api/src/index.ts +++ b/pkgs/api/src/index.ts @@ -1,5 +1,5 @@ import fastify from "fastify"; -import { tsToMpeg4 } from "./utils"; +import { bufferVideo } from "./utils"; const envToLogger = { development: { @@ -26,14 +26,19 @@ declare global { } } -const { NODE_ENV: env = "development" } = process.env; +const { NODE_ENV: env = "development", PORT } = process.env; const app = fastify({ logger: envToLogger[env] }); +app.addContentTypeParser("*", (_, __, done) => done(null)); + app.get("/", async (_, res) => res.redirect("https://bskyx.app")); +// serve 0 bytes favicon so browsers don't spam the server +app.get("/favicon.ico", (_, res) => res.send("")); + app.get<{ Params: { "*": string } }>( - "/generate/*", + "/*", { schema: { params: { @@ -46,41 +51,27 @@ app.get<{ Params: { "*": string } }>( }, }, async (req, res) => { - // Idk anymore - let urls = Buffer.from(req.params["*"], 'base64').toString().split(";"); + const payload = Buffer.from(req.params["*"], "base64") + .toString() + .split(";"); - // Remove .mp4 extension if it exists - if (urls.at(-1)?.endsWith(".mp4")) { - urls = urls.slice(0, -1); - urls.push(urls.at(-1)?.slice(0, -4) as string); - } + const [did, id, quality] = payload; - urls = urls.map((url) => decodeURIComponent(url)).map((url) => url.replaceAll('\uFFFD', '')); + const url = `https://video.bsky.app/watch/${did}/${id}/${quality}/video.m3u8`; - const result = await Promise.allSettled( - urls.map((url) => fetch(url).then((res) => res.arrayBuffer())) - ); + try { + bufferVideo(url, res); - if (result.some((res) => res.status === "rejected")) { + res.header("Content-Type", "video/mp4"); + res.header("Cache-Control", "public, max-age=604800"); + return res; + } catch (error) { + console.error(error); res.status(400).send({ error: "Failed to fetch video" }); - return; } - - const buffers = result.map((res) => - res.status === "fulfilled" ? new Uint8Array(res.value) : new Uint8Array(0) - ); - - const video = await tsToMpeg4(buffers); - - // const fileName = `video-${new Date().toUTCString()}.mp4`; - - res.header("Content-Type", "video/mp4"); - res.header("Cache-Control", "public, max-age=604800"); - // res.header("Content-Disposition", `attachment; filename=${fileName}`); - res.send(video); } ); -const port = process.env.PORT ? Number(process.env.PORT) : 3000; +const port = PORT ? Number(PORT) : 3000; app.listen({ port, host: "0.0.0.0" }); diff --git a/pkgs/api/src/utils.ts b/pkgs/api/src/utils.ts index 6678248..c1f4d2e 100644 --- a/pkgs/api/src/utils.ts +++ b/pkgs/api/src/utils.ts @@ -1,52 +1,53 @@ -import ffmpeg from "fluent-ffmpeg"; -import ffmpegPath from "ffmpeg-static"; +import { type ChildProcess, spawn as s } from "node:child_process"; -import { PassThrough } from "node:stream"; -import fs from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; +import { type FastifyReply } from "fastify"; +import type { Writable, Readable } from "node:stream"; -ffmpeg.setFfmpegPath(ffmpegPath as string); +export function bufferVideo(masterUrl: string, res: FastifyReply) { + let process: ChildProcess; + const cleanup = () => { + process?.kill("SIGTERM"); + setTimeout(() => process?.kill("SIGKILL"), 5000); + res.raw.end(); + }; -export function tsToMpeg4(buffers: Uint8Array[]): Promise { - return new Promise((res, rej) => { - const input = new PassThrough(); + const args = [ + "-loglevel", + "-8", + "-i", + masterUrl, + "-c:v", + "copy", + "-c:a", + "aac", + "-bsf:a", + "aac_adtstoasc", + "-movflags", + "faststart+frag_keyframe+empty_moov", + "-f", + "mp4", + "pipe:3", + ]; - buffers.forEach((b) => input.write(b)); - - input.end(); - - const tempFilePath = path.join(tmpdir(), `output-${Date.now()}.mp4`); - - ffmpeg(input) - .outputOption( - "-c", - "copy", - "-movflags", - "faststart", - "-preset", - "ultrafast" - ) - .on("end", async () => { - try { - const ob = await fs.readFile(tempFilePath); - await fs.unlink(tempFilePath); - res(ob); - } catch (e) { - rej(e); - } - }) - .on("error", async (err, stdout, stderr) => { - console.error("Error:", err.message); - console.error("ffmpeg stdout:", stdout); - console.error("ffmpeg stderr:", stderr); - try { - await fs.unlink(tempFilePath); - } catch (e) { - rej(e); - } - rej(err); - }) - .save(tempFilePath); + process = s("ffmpeg", args, { + windowsHide: true, + stdio: ["inherit", "inherit", "inherit", "pipe"], }); + + const [, , , stream] = process.stdio; + pipe(stream, res.raw, cleanup); + process.on("close", cleanup); + process.on("exit", cleanup); + res.raw.on("finish", cleanup); + return stream; +} + +function pipe( + from: Readable | Writable | undefined | null, + to: NodeJS.WritableStream, + cleanup: () => void +) { + from?.on("error", cleanup).on("close", cleanup); + to.on("error", cleanup).on("close", cleanup); + from?.pipe(to); } diff --git a/pkgs/app/src/components/Post.tsx b/pkgs/app/src/components/Post.tsx index 9eab0cb..1cd4394 100644 --- a/pkgs/app/src/components/Post.tsx +++ b/pkgs/app/src/components/Post.tsx @@ -5,7 +5,7 @@ import { OEmbedTypes } from "../routes/getOEmbed"; import { parseEmbedImages } from "../lib/parseEmbedImages"; import { parseEmbedDescription } from "../lib/parseEmbedDescription"; import { StreamInfo } from "../lib/processVideoEmbed"; -import { join } from "../lib/utils"; +import { checkType, join } from "../lib/utils"; interface PostProps { post: AppBskyFeedDefs.PostView; @@ -21,6 +21,16 @@ const Meta = ({ post }: { post: AppBskyFeedDefs.PostView }) => ( ); +const constructVideoUrl = (streamInfo: StreamInfo, apiUrl: string) => { + const url = new URL(streamInfo.masterUri); + + const [did, id, quality] = url.pathname.split("/").slice(2); + + const parts = [did, id, quality]; + + return `${apiUrl}generate/${btoa(join(parts, ";"))}.mp4`; +}; + const Video = ({ streamInfo, apiUrl, @@ -34,8 +44,7 @@ const Video = ({ post: AppBskyFeedDefs.PostView; description: string; }) => { - // Discord can't handle query params in the URL, so i have to do this 🔥beautiful mess🔥 - const url = `${apiUrl}generate/${btoa(join(streamInfo.uri, ";"))}.mp4`; + const url = constructVideoUrl(streamInfo, apiUrl); return ( <> @@ -109,7 +118,21 @@ export const Post = ({ }: PostProps) => { const images = parseEmbedImages(post); const isAuthor = images === post.author.avatar; - const description = parseEmbedDescription(post); + let description = parseEmbedDescription(post); + const isVideo = checkType( + "app.bsky.embed.video", + post.embed?.media ?? post.embed + ); + const streamInfo = videoMetadata?.at(-1); + const isTooLong = isVideo && streamInfo!.uri.length > 4; + const shouldOverrideForVideo = isVideo && isTooLong; + + let videoUrl; + + if (isVideo && isTooLong) { + videoUrl = constructVideoUrl(streamInfo!, apiUrl); + description += `\n[Video is too long to embed!]`; + } return ( @@ -125,23 +148,24 @@ export const Post = ({ /> - {/* */} {!isAuthor && } - {images.length !== 0 && !videoMetadata && } + {images.length !== 0 && (shouldOverrideForVideo || !isVideo) && ( + + )} - {videoMetadata && ( + {isVideo && streamInfo!.uri.length <= 4 && ( diff --git a/pkgs/app/src/lib/parseEmbedDescription.ts b/pkgs/app/src/lib/parseEmbedDescription.ts index 7e47ca9..6032a22 100644 --- a/pkgs/app/src/lib/parseEmbedDescription.ts +++ b/pkgs/app/src/lib/parseEmbedDescription.ts @@ -4,6 +4,7 @@ import { AppBskyFeedDefs, AppBskyFeedPost, } from "@atproto/api"; +import { indent } from "./utils"; export function parseEmbedDescription(post: AppBskyFeedDefs.PostView) { if (AppBskyFeedPost.isRecord(post.record)) { @@ -15,7 +16,7 @@ export function parseEmbedDescription(post: AppBskyFeedDefs.PostView) { ); if (isViewRecord) { // @ts-expect-error For some reason the original post value is typed as {} - return `${post.record.text}\n\nQuoting @${post.embed.record.author.handle}\n➥ ${post.embed.record.value.text}`; + return `${post.record.text}\n\nQuoting @${post.embed.record.author.handle}\n➥${indent(post.embed.record.value.text, 2)}`; } } } @@ -29,7 +30,7 @@ export function parseEmbedDescription(post: AppBskyFeedDefs.PostView) { ); if (isViewRecord) { // @ts-expect-error For some reason the original post value is typed as {} - return `${post.record.text}\n\nQuoting @${post.embed.record.record.author.handle}\n➥ ${post.embed.record.record.value.text}`; + return `${post.record.text}\n\nQuoting @${post.embed.record.record.author.handle}\n➥${indent(post.embed.record.record.value.text, 2)}`; } } } diff --git a/pkgs/app/src/lib/processVideoEmbed.ts b/pkgs/app/src/lib/processVideoEmbed.ts index b7d2db8..ff918b8 100644 --- a/pkgs/app/src/lib/processVideoEmbed.ts +++ b/pkgs/app/src/lib/processVideoEmbed.ts @@ -8,6 +8,7 @@ export interface StreamInfo { }; codecs: string; uri: string | string[]; + masterUri: string; } export interface M3U8Data { @@ -66,6 +67,7 @@ async function parseM3U8( }, codecs: "", uri: "", + masterUri: "", }; for (const attrib of attribs) { @@ -88,6 +90,8 @@ async function parseM3U8( } else if (line.includes("m3u8")) { const resolvedUrl = `${initalUrl}/${line}`; + streams.at(-1)!.masterUri = resolvedUrl; + const cont = await fetch(resolvedUrl).then((res) => res.text()); const parsed = await parseM3U8(removeLastPathSegment(resolvedUrl), cont); @@ -102,6 +106,7 @@ async function parseM3U8( }, codecs: "", uri: `${initalUrl}/${line}`, + masterUri: initalUrl, }); } else { // Discard diff --git a/pkgs/app/src/lib/utils.ts b/pkgs/app/src/lib/utils.ts index dc6f1a1..2987c5a 100644 --- a/pkgs/app/src/lib/utils.ts +++ b/pkgs/app/src/lib/utils.ts @@ -12,4 +12,10 @@ export const join = (t: string | string[], s: string) => Array.isArray(t) ? t.join(s) : t; export const checkType = (t: string, o: any) => - (typeof o.$type === "string" && o.$type.startsWith(t)) || o.$type === t; + (typeof o?.$type === "string" && o?.$type.startsWith(t)) || o?.$type === t; + +export const indent = (s: string, n: number) => + s + .split("\n") + .map((l) => " ".repeat(n) + l) + .join("\n"); diff --git a/pkgs/app/src/routes/getOEmbed.ts b/pkgs/app/src/routes/getOEmbed.ts index 9bd9fad..f531704 100644 --- a/pkgs/app/src/routes/getOEmbed.ts +++ b/pkgs/app/src/routes/getOEmbed.ts @@ -23,11 +23,12 @@ export const getOEmbed: Handler = async (c) => { } if (type === OEmbedTypes.Post) { - const { replies, reposts, likes } = c.req.query(); + const { replies, reposts, likes, videoUrl } = c.req.query(); return c.json({ - author_name: `🗨️ ${replies} ♻️ ${reposts} 💙 ${likes}`, ...defaults, + author_name: `🗨️ ${replies} ♻️ ${reposts} 💙 ${likes}`, + provider_url: videoUrl ? videoUrl : defaults.provider_url, }); } if (type === OEmbedTypes.Profile) { diff --git a/pkgs/app/src/routes/getPost.tsx b/pkgs/app/src/routes/getPost.tsx index 6263c27..907beef 100644 --- a/pkgs/app/src/routes/getPost.tsx +++ b/pkgs/app/src/routes/getPost.tsx @@ -2,7 +2,7 @@ import { Handler } from "hono"; import { HTTPException } from "hono/http-exception"; import { fetchPost } from "../lib/fetchPostData"; import { Post } from "../components/Post"; -import { processVideoEmbed, StreamInfo, VideoMedia } from "../lib/processVideoEmbed"; +import { processVideoEmbed, StreamInfo } from "../lib/processVideoEmbed"; import { checkType } from "../lib/utils"; export const getPost: Handler< @@ -27,8 +27,10 @@ export const getPost: Handler< checkType("app.bsky.embed.video", fetchedPost.embed) || checkType("app.bsky.embed.video", fetchedPost.embed?.media) ) { - // @ts-expect-error - videoMetaData = await processVideoEmbed(fetchedPost.embed?.media || fetchedPost.embed); + videoMetaData = await processVideoEmbed( + // @ts-expect-error + fetchedPost.embed?.media ?? fetchedPost.embed + ); } return c.html(