video support (#7)

real
This commit is contained in:
Lexie
2024-09-14 19:08:20 +02:00
committed by GitHub
parent 8dd4613997
commit 88dc790567
27 changed files with 1098 additions and 5 deletions

13
pkgs/api/Dockerfile Normal file
View File

@@ -0,0 +1,13 @@
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm i -g pnpm && pnpm install
COPY . .
RUN pnpm build
CMD [ "node", "dist/index.js" ]

View File

@@ -0,0 +1,10 @@
services:
api:
build:
context: .
dockerfile: Dockerfile
ports:
- "2598:3000"
environment:
- NODE_ENV=production
- PORT=3000

24
pkgs/api/package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "api",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc"
},
"keywords": [],
"author": "Lexedia",
"license": "ISC",
"dependencies": {
"fastify": "^4.28.1",
"ffmpeg-static": "^5.2.0",
"fluent-ffmpeg": "^2.1.3"
},
"devDependencies": {
"@types/fluent-ffmpeg": "^2.1.26",
"@types/node": "^22.5.4",
"pino-pretty": "^11.2.2",
"typescript": "^5.6.2"
}
}

741
pkgs/api/pnpm-lock.yaml generated Normal file
View File

@@ -0,0 +1,741 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
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
pino-pretty:
specifier: ^11.2.2
version: 11.2.2
typescript:
specifier: ^5.6.2
version: 5.6.2
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==}
'@fastify/error@3.4.1':
resolution: {integrity: sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==}
'@fastify/fast-json-stringify-compiler@4.3.0':
resolution: {integrity: sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==}
'@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==}
abort-controller@3.0.0:
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
engines: {node: '>=6.5'}
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:
ajv: ^8.0.0
peerDependenciesMeta:
ajv:
optional: true
ajv-formats@3.0.1:
resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
peerDependencies:
ajv: ^8.0.0
peerDependenciesMeta:
ajv:
optional: true
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'}
avvio@8.4.0:
resolution: {integrity: sha512-CDSwaxINFy59iNwhYnkvALBwZiTydGkOecZyPkqBpABYR1KqGEsET0VOOYDwtleZSUIdeY36DC2bSZ24CO1igA==}
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'}
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'}
events@3.3.0:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
fast-content-type-parse@1.1.0:
resolution: {integrity: sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==}
fast-copy@3.0.2:
resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==}
fast-decode-uri-component@1.0.1:
resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==}
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
fast-json-stringify@5.16.1:
resolution: {integrity: sha512-KAdnLvy1yu/XrRtP+LJnxbBGrhN+xXu+gt3EUvZhYGKCr3lFHq/7UFJHHFgmJKoqlh6B40bZLEv7w46B0mqn1g==}
fast-querystring@1.1.2:
resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==}
fast-redact@3.5.0:
resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==}
engines: {node: '>=6'}
fast-safe-stringify@2.1.1:
resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
fast-uri@2.4.0:
resolution: {integrity: sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==}
fast-uri@3.0.1:
resolution: {integrity: sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==}
fastify@4.28.1:
resolution: {integrity: sha512-kFWUtpNr4i7t5vY2EJPCN2KgMVpuqfU4NjnJNCgiNB900oiDeYqaNDRcAfeBbOF5hGixixxcKnOU4KN9z6QncQ==}
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'}
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'}
json-schema-ref-resolver@1.0.1:
resolution: {integrity: sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==}
json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
light-my-request@5.13.0:
resolution: {integrity: sha512-9IjUN9ZyCS9pTG+KqTDEQo68Sui2lHsYBrfMyVUTTZ3XhH8PMZq7xO94Kr+eP9dhi/kcKsx4N41p2IXEBil1pQ==}
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'}
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==}
pino-pretty@11.2.2:
resolution: {integrity: sha512-2FnyGir8nAJAqD3srROdrF1J5BIcMT4nwj7hHSc60El6Uxlym00UbCCd8pYIterstVBFlMyF1yFV8XdGIPbj4A==}
hasBin: true
pino-std-serializers@7.0.0:
resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==}
pino@9.4.0:
resolution: {integrity: sha512-nbkQb5+9YPhQRz/BeQmrWpEknAaqjpAqRK8NwJpmrX/JHu7JuZC5G1CeAwJDJfGes4h+YihC6in3Q2nGb+Y09w==}
hasBin: true
process-warning@3.0.0:
resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==}
process-warning@4.0.0:
resolution: {integrity: sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==}
process@0.11.10:
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'}
pump@3.0.2:
resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
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}
real-require@0.2.0:
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
engines: {node: '>= 12.13.0'}
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
ret@0.4.3:
resolution: {integrity: sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==}
engines: {node: '>=10'}
reusify@1.0.4:
resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
rfdc@1.4.1:
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
safe-regex2@3.1.0:
resolution: {integrity: sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==}
safe-stable-stringify@2.5.0:
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
engines: {node: '>=10'}
secure-json-parse@2.7.0:
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
semver@7.6.3:
resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==}
engines: {node: '>=10'}
hasBin: true
set-cookie-parser@2.7.0:
resolution: {integrity: sha512-lXLOiqpkUumhRdFF3k1osNXCy9akgx/dyPZ5p8qAg9seJzXr5ZrlqZuWIMuY6ejOsVLE6flJ5/h3lsn57fQ/PQ==}
sonic-boom@4.1.0:
resolution: {integrity: sha512-NGipjjRicyJJ03rPiZCJYjwlsuP2d1/5QUviozRXC7S3WdVWNK5e3Ojieb9CCyfhq2UC+3+SRd9nG3I2lPRvUw==}
split2@4.2.0:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
strip-json-comments@3.1.1:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
thread-stream@3.1.0:
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
toad-cache@3.7.0:
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'}
hasBin: true
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
ajv-formats: 2.1.1(ajv@8.17.1)
fast-uri: 2.4.0
'@fastify/error@3.4.1': {}
'@fastify/fast-json-stringify-compiler@4.3.0':
dependencies:
fast-json-stringify: 5.16.1
'@fastify/merge-json-schemas@0.1.1':
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
abort-controller@3.0.0:
dependencies:
event-target-shim: 5.0.1
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
ajv-formats@3.0.1(ajv@8.17.1):
optionalDependencies:
ajv: 8.17.1
ajv@8.17.1:
dependencies:
fast-deep-equal: 3.1.3
fast-uri: 3.0.1
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:
dependencies:
'@fastify/error': 3.4.1
fastq: 1.17.1
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: {}
fast-content-type-parse@1.1.0: {}
fast-copy@3.0.2: {}
fast-decode-uri-component@1.0.1: {}
fast-deep-equal@3.1.3: {}
fast-json-stringify@5.16.1:
dependencies:
'@fastify/merge-json-schemas': 0.1.1
ajv: 8.17.1
ajv-formats: 3.0.1(ajv@8.17.1)
fast-deep-equal: 3.1.3
fast-uri: 2.4.0
json-schema-ref-resolver: 1.0.1
rfdc: 1.4.1
fast-querystring@1.1.2:
dependencies:
fast-decode-uri-component: 1.0.1
fast-redact@3.5.0: {}
fast-safe-stringify@2.1.1: {}
fast-uri@2.4.0: {}
fast-uri@3.0.1: {}
fastify@4.28.1:
dependencies:
'@fastify/ajv-compiler': 3.6.0
'@fastify/error': 3.4.1
'@fastify/fast-json-stringify-compiler': 4.3.0
abstract-logging: 2.0.1
avvio: 8.4.0
fast-content-type-parse: 1.1.0
fast-json-stringify: 5.16.1
find-my-way: 8.2.0
light-my-request: 5.13.0
pino: 9.4.0
process-warning: 3.0.0
proxy-addr: 2.0.7
rfdc: 1.4.1
secure-json-parse: 2.7.0
semver: 7.6.3
toad-cache: 3.7.0
fastq@1.17.1:
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:
dependencies:
fast-deep-equal: 3.1.3
json-schema-traverse@1.0.0: {}
light-my-request@5.13.0:
dependencies:
cookie: 0.6.0
process-warning: 3.0.0
set-cookie-parser: 2.7.0
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
split2: 4.2.0
pino-pretty@11.2.2:
dependencies:
colorette: 2.0.20
dateformat: 4.6.3
fast-copy: 3.0.2
fast-safe-stringify: 2.1.1
help-me: 5.0.0
joycon: 3.1.1
minimist: 1.2.8
on-exit-leak-free: 2.1.2
pino-abstract-transport: 1.2.0
pump: 3.0.2
readable-stream: 4.5.2
secure-json-parse: 2.7.0
sonic-boom: 4.1.0
strip-json-comments: 3.1.1
pino-std-serializers@7.0.0: {}
pino@9.4.0:
dependencies:
atomic-sleep: 1.0.0
fast-redact: 3.5.0
on-exit-leak-free: 2.1.2
pino-abstract-transport: 1.2.0
pino-std-serializers: 7.0.0
process-warning: 4.0.0
quick-format-unescaped: 4.0.4
real-require: 0.2.0
safe-stable-stringify: 2.5.0
sonic-boom: 4.1.0
thread-stream: 3.1.0
process-warning@3.0.0: {}
process-warning@4.0.0: {}
process@0.11.10: {}
progress@2.0.3: {}
proxy-addr@2.0.7:
dependencies:
forwarded: 0.2.0
ipaddr.js: 1.9.1
pump@3.0.2:
dependencies:
end-of-stream: 1.4.4
once: 1.4.0
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
buffer: 6.0.3
events: 3.3.0
process: 0.11.10
string_decoder: 1.3.0
real-require@0.2.0: {}
require-from-string@2.0.2: {}
ret@0.4.3: {}
reusify@1.0.4: {}
rfdc@1.4.1: {}
safe-buffer@5.2.1: {}
safe-regex2@3.1.0:
dependencies:
ret: 0.4.3
safe-stable-stringify@2.5.0: {}
secure-json-parse@2.7.0: {}
semver@7.6.3: {}
set-cookie-parser@2.7.0: {}
sonic-boom@4.1.0:
dependencies:
atomic-sleep: 1.0.0
split2@4.2.0: {}
string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1
strip-json-comments@3.1.1: {}
thread-stream@3.1.0:
dependencies:
real-require: 0.2.0
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: {}

69
pkgs/api/src/index.ts Normal file
View File

@@ -0,0 +1,69 @@
import fastify from "fastify";
import { tsToMpeg4 } from "./utils";
const envToLogger = {
development: {
transport: {
target: "pino-pretty",
options: {
translateTime: "HH:MM:ss Z",
ignore: "pid,hostname",
},
},
},
production: true,
test: false,
};
type Env = keyof typeof envToLogger;
declare global {
namespace NodeJS {
interface ProcessEnv {
readonly NODE_ENV: Env;
readonly PORT: string | undefined;
}
}
}
const { NODE_ENV: env = "development" } = process.env;
const app = fastify({ logger: envToLogger[env] });
app.get("/", async (_, res) => res.redirect("https://bskyx.app"));
app.get<{ Params: { "*": string } }>(
"/generate/*",
{
schema: {
params: {
type: "object",
properties: {
"*": { type: "string" },
},
required: ["*"],
},
},
},
async (req, res) => {
let url = req.params["*"];
if (url.endsWith(".mp4")) {
url = url.slice(0, -4);
}
url = decodeURIComponent(url);
const result = await fetch(url).then((res) => res.arrayBuffer());
const video = await tsToMpeg4(Buffer.from(result));
res.header("Content-Type", "video/mp4");
res.header("Cache-Control", "public, max-age=604800");
res.send(video);
}
);
const port = process.env.PORT ? Number(process.env.PORT) : 3000;
app.listen({ port, host: "0.0.0.0" });

50
pkgs/api/src/utils.ts Normal file
View File

@@ -0,0 +1,50 @@
import ffmpeg from "fluent-ffmpeg";
import ffmpegPath from "ffmpeg-static";
import { PassThrough } from "node:stream";
import fs from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
ffmpeg.setFfmpegPath(ffmpegPath as string);
export function tsToMpeg4(buffer: Buffer | Uint8Array): Promise<Buffer> {
return new Promise((res, rej) => {
const input = new PassThrough();
input.end(buffer);
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);
});
}

13
pkgs/api/tsconfig.json Normal file
View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "es2016",
"module": "NodeNext",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"moduleResolution": "NodeNext",
"outDir": "./dist",
},
"include": ["./src/**/*.ts"],
}

View File

@@ -4,7 +4,7 @@
"name": "Wrangler",
"type": "node",
"request": "attach",
"port": 4365,
"port": 8787,
"cwd": "/",
"resolveSourceMapLocations": null,
"attachExistingChildren": false,

View File

@@ -4,11 +4,14 @@ import { Layout } from "./Layout";
import { OEmbedTypes } from "../routes/getOEmbed";
import { parseEmbedImages } from "../lib/parseEmbedImages";
import { parseEmbedDescription } from "../lib/parseEmbedDescription";
import { StreamInfo } from "../lib/processVideoEmbed";
interface PostProps {
post: AppBskyFeedDefs.PostView;
url: string;
appDomain: string;
videoMetadata?: StreamInfo[] | undefined;
apiUrl: string;
}
const Meta = ({ post }: { post: AppBskyFeedDefs.PostView }) => (
@@ -17,6 +20,44 @@ const Meta = ({ post }: { post: AppBskyFeedDefs.PostView }) => (
</>
);
const Video = ({
streamInfo,
apiUrl,
}: {
streamInfo: StreamInfo;
apiUrl: string;
}) => {
const url = `${apiUrl}generate/${encodeURIComponent(streamInfo.uri)}.mp4`;
return (
<>
<meta property="twitter:card" content="player" />
<meta property="twitter:player" content={url} />
<meta property="twitter:player:stream" content={url} />
<meta property="og:type" content="video.other" />
<meta property="og:video" content={url} />
<meta property="og:video:secure_url" content={url} />
<meta property="og:video:type" content="video/mp4" />
<meta
property="og:video:width"
content={streamInfo.resolution.width.toString()}
/>
<meta
property="og:video:height"
content={streamInfo.resolution.height.toString()}
/>
<meta
property="twitter:player:width"
content={streamInfo.resolution.width.toString()}
/>
<meta
property="twitter:player:height"
content={streamInfo.resolution.height.toString()}
/>
</>
);
};
const Images = ({
images,
}: {
@@ -39,7 +80,13 @@ const Images = ({
</>
);
export const Post = ({ post, url, appDomain }: PostProps) => {
export const Post = ({
post,
url,
appDomain,
videoMetadata,
apiUrl,
}: PostProps) => {
const images = parseEmbedImages(post);
const isAuthor = images === post.author.avatar;
@@ -61,7 +108,11 @@ export const Post = ({ post, url, appDomain }: PostProps) => {
{!isAuthor && <Meta post={post} />}
{images.length !== 0 && <Images images={images} />}
{images.length !== 0 && !videoMetadata && <Images images={images} />}
{videoMetadata && (
<Video apiUrl={apiUrl} streamInfo={videoMetadata.at(-1)!} />
)}
<link
rel="alternate"

View File

@@ -1,5 +1,5 @@
import { BskyAgent } from "@atproto/api";
import type {KVNamespace} from '@cloudflare/workers-types';
import type { KVNamespace } from "@cloudflare/workers-types";
declare global {
interface Env {
@@ -8,6 +8,7 @@ declare global {
BSKY_AUTH_USERNAME: string;
BSKY_AUTH_PASSWORD: string;
VIXBLUESKY_APP_DOMAIN: string;
VIXBLUESKY_API_URL: string;
bskyx: KVNamespace;
};
Variables: {

View File

@@ -0,0 +1,106 @@
import { AppBskyFeedDefs } from "@atproto/api";
export interface StreamInfo {
bandwidth: number;
resolution: {
width: number;
height: number;
};
codecs: string;
uri: string;
}
export interface M3U8Data {
version: number;
streams: StreamInfo[];
}
export async function processVideoEmbed(post: AppBskyFeedDefs.PostView) {
const videoUrl = post.embed?.playlist as string | undefined;
if (!videoUrl) {
return;
}
const contents = await fetch(videoUrl).then((res) => res.text());
const initalUrl = removeLastPathSegment(videoUrl);
const { streams } = await parseM3U8(initalUrl, contents);
return streams;
}
async function parseM3U8(
initalUrl: string,
contents: string
): Promise<M3U8Data> {
const lines = contents
.split("\n")
.map((l) => l.trim())
.filter((l) => l.length > 0);
let version = 0;
const streams: StreamInfo[] = [];
for (const line of lines) {
if (line.startsWith("#EXT-X-VERSION:")) {
version = Number(line.split(":")[1]);
} else if (line.startsWith("#EXT-X-STREAM-INF:")) {
const attribs = line.split(":")[1].split(",");
const sInfo: StreamInfo = {
bandwidth: 0,
resolution: {
width: 0,
height: 0,
},
codecs: "",
uri: "",
};
for (const attrib of attribs) {
const [key, value] = attrib.split("=");
switch (key) {
case "BANDWIDTH":
sInfo.bandwidth = Number(value);
break;
case "RESOLUTION":
const [width, height] = value.split("x").map(Number);
sInfo.resolution = { width, height };
break;
case "CODECS":
sInfo.codecs = value.replaceAll('"', "");
break;
}
}
streams.push(sInfo);
} else if (line.includes("m3u8")) {
const resolvedUrl = `${initalUrl}/${line}`;
const cont = await fetch(resolvedUrl).then((res) => res.text());
const parsed = await parseM3U8(removeLastPathSegment(resolvedUrl), cont);
streams.at(-1)!.uri = parsed.streams[0].uri;
} else if (line.includes(".ts")) {
streams.push({
bandwidth: 0,
resolution: {
width: 0,
height: 0,
},
codecs: "",
uri: `${initalUrl}/${line}`,
});
} else {
// Discard
continue;
}
}
return { version, streams };
}
function removeLastPathSegment(url: string) {
return url.slice(0, url.lastIndexOf("/"));
}

View File

@@ -2,6 +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 } from "../lib/processVideoEmbed";
export const getPost: Handler<
Env,
@@ -16,11 +17,24 @@ export const getPost: Handler<
});
}
const fetchedPost = data.posts[0];
let videoMetaData: StreamInfo[] | undefined;
if (
typeof fetchedPost.embed?.$type === "string" &&
fetchedPost.embed?.$type.startsWith("app.bsky.embed.video")
) {
videoMetaData = await processVideoEmbed(fetchedPost);
}
return c.html(
<Post
post={data.posts[0]}
post={fetchedPost}
url={c.req.path}
appDomain={c.env.VIXBLUESKY_APP_DOMAIN}
videoMetadata={videoMetaData}
apiUrl={c.env.VIXBLUESKY_API_URL}
/>
);
};

View File

@@ -8,6 +8,7 @@ route = { pattern = "bskyx.app/*", zone_name = "bskyx.app" }
[vars]
BSKY_SERVICE_URL="https://bsky.social/"
VIXBLUESKY_APP_DOMAIN="bskyx.app"
VIXBLUESKY_API_URL="https://api.bskyx.app/"
[[kv_namespaces]]
binding = "bskyx"