cursedbelt-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/dist/server/activity/index.d.ts +36 -0
- package/dist/server/activity/index.js +36 -0
- package/dist/server/activity/migrations.d.ts +21 -0
- package/dist/server/activity/migrations.js +55 -0
- package/dist/server/activity/shareSource.d.ts +11 -0
- package/dist/server/activity/shareSource.js +45 -0
- package/dist/server/activity/store.d.ts +123 -0
- package/dist/server/activity/store.js +178 -0
- package/dist/server/app.d.ts +56 -0
- package/dist/server/app.js +45 -0
- package/dist/server/audit/index.d.ts +2 -0
- package/dist/server/audit/index.js +2 -0
- package/dist/server/audit/migrations.d.ts +2 -0
- package/dist/server/audit/migrations.js +79 -0
- package/dist/server/audit/sqliteSink.d.ts +36 -0
- package/dist/server/audit/sqliteSink.js +95 -0
- package/dist/server/audit/sqliteSink.test.d.ts +1 -0
- package/dist/server/audit/sqliteSink.test.js +180 -0
- package/dist/server/auth/cookie.d.ts +22 -0
- package/dist/server/auth/cookie.js +35 -0
- package/dist/server/auth/csrf.d.ts +42 -0
- package/dist/server/auth/csrf.js +36 -0
- package/dist/server/auth/fileTokenSigner.d.ts +10 -0
- package/dist/server/auth/fileTokenSigner.js +22 -0
- package/dist/server/auth/getOnly.d.ts +20 -0
- package/dist/server/auth/getOnly.js +27 -0
- package/dist/server/auth/index.d.ts +12 -0
- package/dist/server/auth/index.js +15 -0
- package/dist/server/auth/jwt.d.ts +18 -0
- package/dist/server/auth/jwt.js +56 -0
- package/dist/server/auth/loginThrottle.d.ts +118 -0
- package/dist/server/auth/loginThrottle.js +204 -0
- package/dist/server/auth/middleware.d.ts +14 -0
- package/dist/server/auth/middleware.js +47 -0
- package/dist/server/auth/password.d.ts +14 -0
- package/dist/server/auth/password.js +65 -0
- package/dist/server/auth/sessionStore.d.ts +19 -0
- package/dist/server/auth/sessionStore.js +221 -0
- package/dist/server/auth/totp.d.ts +34 -0
- package/dist/server/auth/totp.js +81 -0
- package/dist/server/auth/types.d.ts +133 -0
- package/dist/server/auth/types.js +1 -0
- package/dist/server/auth/vault.d.ts +27 -0
- package/dist/server/auth/vault.js +57 -0
- package/dist/server/context.d.ts +35 -0
- package/dist/server/context.js +1 -0
- package/dist/server/ctgr.d.ts +32 -0
- package/dist/server/ctgr.js +69 -0
- package/dist/server/db/kysely.d.ts +14 -0
- package/dist/server/db/kysely.js +16 -0
- package/dist/server/db/plumbingTypes.d.ts +116 -0
- package/dist/server/db/plumbingTypes.js +12 -0
- package/dist/server/dropzone/duplicates.d.ts +82 -0
- package/dist/server/dropzone/duplicates.js +90 -0
- package/dist/server/dropzone/index.d.ts +13 -0
- package/dist/server/dropzone/index.js +13 -0
- package/dist/server/dropzone/scan.d.ts +166 -0
- package/dist/server/dropzone/scan.js +272 -0
- package/dist/server/errors.d.ts +7 -0
- package/dist/server/errors.js +7 -0
- package/dist/server/guard/argon2Binding.d.ts +97 -0
- package/dist/server/guard/argon2Binding.js +138 -0
- package/dist/server/guard/index.d.ts +4 -0
- package/dist/server/guard/index.js +6 -0
- package/dist/server/guard/kdfWorker.d.ts +101 -0
- package/dist/server/guard/kdfWorker.js +301 -0
- package/dist/server/guard/kdfWorkerEntry.d.ts +1 -0
- package/dist/server/guard/kdfWorkerEntry.js +15 -0
- package/dist/server/guard/passwordVerifier.d.ts +65 -0
- package/dist/server/guard/passwordVerifier.js +122 -0
- package/dist/server/guard/revocationStore.d.ts +132 -0
- package/dist/server/guard/revocationStore.js +211 -0
- package/dist/server/guard/sessionSigner.d.ts +132 -0
- package/dist/server/guard/sessionSigner.js +303 -0
- package/dist/server/guard/verifier.d.ts +21 -0
- package/dist/server/guard/verifier.js +1 -0
- package/dist/server/incidents/channels.d.ts +161 -0
- package/dist/server/incidents/channels.js +357 -0
- package/dist/server/incidents/checks.d.ts +61 -0
- package/dist/server/incidents/checks.js +55 -0
- package/dist/server/incidents/index.d.ts +51 -0
- package/dist/server/incidents/index.js +51 -0
- package/dist/server/incidents/migrations.d.ts +20 -0
- package/dist/server/incidents/migrations.js +58 -0
- package/dist/server/incidents/notifier.d.ts +79 -0
- package/dist/server/incidents/notifier.js +141 -0
- package/dist/server/incidents/reporter.d.ts +60 -0
- package/dist/server/incidents/reporter.js +49 -0
- package/dist/server/incidents/routes.d.ts +91 -0
- package/dist/server/incidents/routes.js +202 -0
- package/dist/server/incidents/store.d.ts +75 -0
- package/dist/server/incidents/store.js +176 -0
- package/dist/server/incidents/types.d.ts +107 -0
- package/dist/server/incidents/types.js +32 -0
- package/dist/server/index.d.ts +25 -0
- package/dist/server/index.js +53 -0
- package/dist/server/jobs/fireQueue.d.ts +12 -0
- package/dist/server/jobs/fireQueue.js +37 -0
- package/dist/server/jobs/index.d.ts +6 -0
- package/dist/server/jobs/index.js +10 -0
- package/dist/server/jobs/logger.d.ts +10 -0
- package/dist/server/jobs/logger.js +14 -0
- package/dist/server/jobs/plainjobAdapter.d.ts +19 -0
- package/dist/server/jobs/plainjobAdapter.js +48 -0
- package/dist/server/jobs/polling.d.ts +17 -0
- package/dist/server/jobs/polling.js +79 -0
- package/dist/server/jobs/runner.d.ts +24 -0
- package/dist/server/jobs/runner.js +186 -0
- package/dist/server/jobs/timeout.d.ts +28 -0
- package/dist/server/jobs/timeout.js +47 -0
- package/dist/server/jobs/types.d.ts +167 -0
- package/dist/server/jobs/types.js +12 -0
- package/dist/server/master-lock/guard.d.ts +53 -0
- package/dist/server/master-lock/guard.js +205 -0
- package/dist/server/master-lock/index.d.ts +21 -0
- package/dist/server/master-lock/index.js +21 -0
- package/dist/server/master-lock/lockPage.d.ts +34 -0
- package/dist/server/master-lock/lockPage.js +281 -0
- package/dist/server/master-lock/masterLock.d.ts +294 -0
- package/dist/server/master-lock/masterLock.js +523 -0
- package/dist/server/master-lock/principals.d.ts +76 -0
- package/dist/server/master-lock/principals.js +157 -0
- package/dist/server/master-lock/seed.d.ts +47 -0
- package/dist/server/master-lock/seed.js +93 -0
- package/dist/server/master-lock/store.d.ts +19 -0
- package/dist/server/master-lock/store.js +20 -0
- package/dist/server/media-bun/image.d.ts +7 -0
- package/dist/server/media-bun/image.js +96 -0
- package/dist/server/media-bun/index.d.ts +4 -0
- package/dist/server/media-bun/index.js +10 -0
- package/dist/server/media-bun/streaming.d.ts +86 -0
- package/dist/server/media-bun/streaming.js +237 -0
- package/dist/server/media-bun/video.d.ts +35 -0
- package/dist/server/media-bun/video.js +181 -0
- package/dist/server/metrics/metricsBuffer.d.ts +39 -0
- package/dist/server/metrics/metricsBuffer.js +36 -0
- package/dist/server/metrics/normalizeRoute.d.ts +5 -0
- package/dist/server/metrics/normalizeRoute.js +58 -0
- package/dist/server/middleware/correlationId.d.ts +9 -0
- package/dist/server/middleware/correlationId.js +20 -0
- package/dist/server/middleware/corsAllowlist.d.ts +38 -0
- package/dist/server/middleware/corsAllowlist.js +37 -0
- package/dist/server/middleware/errorEnvelope.d.ts +17 -0
- package/dist/server/middleware/errorEnvelope.js +32 -0
- package/dist/server/middleware/index.d.ts +9 -0
- package/dist/server/middleware/index.js +10 -0
- package/dist/server/middleware/localOrigin.d.ts +71 -0
- package/dist/server/middleware/localOrigin.js +119 -0
- package/dist/server/middleware/rateLimit.d.ts +56 -0
- package/dist/server/middleware/rateLimit.js +86 -0
- package/dist/server/middleware/requestLogger.d.ts +29 -0
- package/dist/server/middleware/requestLogger.js +47 -0
- package/dist/server/middleware/securityHeaders.d.ts +29 -0
- package/dist/server/middleware/securityHeaders.js +44 -0
- package/dist/server/middleware/zValidator.d.ts +29 -0
- package/dist/server/middleware/zValidator.js +57 -0
- package/dist/server/migration/runner.d.ts +42 -0
- package/dist/server/migration/runner.js +70 -0
- package/dist/server/migration/types.d.ts +16 -0
- package/dist/server/migration/types.js +1 -0
- package/dist/server/net/outboundGuard.d.ts +30 -0
- package/dist/server/net/outboundGuard.js +199 -0
- package/dist/server/notifications/hub.d.ts +67 -0
- package/dist/server/notifications/hub.js +187 -0
- package/dist/server/notifications/index.d.ts +47 -0
- package/dist/server/notifications/index.js +46 -0
- package/dist/server/notifications/migrations.d.ts +13 -0
- package/dist/server/notifications/migrations.js +20 -0
- package/dist/server/notifications/routes.d.ts +58 -0
- package/dist/server/notifications/routes.js +113 -0
- package/dist/server/notifications/service.d.ts +58 -0
- package/dist/server/notifications/service.js +41 -0
- package/dist/server/paths/appDataHome.d.ts +19 -0
- package/dist/server/paths/appDataHome.js +68 -0
- package/dist/server/private-media/crypto.d.ts +37 -0
- package/dist/server/private-media/crypto.js +111 -0
- package/dist/server/private-media/index-store.d.ts +31 -0
- package/dist/server/private-media/index-store.js +36 -0
- package/dist/server/private-media/mountPrivateRoutes.d.ts +26 -0
- package/dist/server/private-media/mountPrivateRoutes.js +219 -0
- package/dist/server/private-media/pipeline.d.ts +37 -0
- package/dist/server/private-media/pipeline.js +97 -0
- package/dist/server/private-media/unlock.d.ts +52 -0
- package/dist/server/private-media/unlock.js +55 -0
- package/dist/server/resume/index.d.ts +2 -0
- package/dist/server/resume/index.js +2 -0
- package/dist/server/resume/routes.d.ts +41 -0
- package/dist/server/resume/routes.js +50 -0
- package/dist/server/resume/store.d.ts +37 -0
- package/dist/server/resume/store.js +38 -0
- package/dist/server/retention/archive.d.ts +78 -0
- package/dist/server/retention/archive.js +284 -0
- package/dist/server/retention/fleet.d.ts +50 -0
- package/dist/server/retention/fleet.js +66 -0
- package/dist/server/retention/index.d.ts +38 -0
- package/dist/server/retention/index.js +38 -0
- package/dist/server/retention/policy.d.ts +263 -0
- package/dist/server/retention/policy.js +264 -0
- package/dist/server/retention/store.d.ts +101 -0
- package/dist/server/retention/store.js +251 -0
- package/dist/server/retention/sweeper.d.ts +69 -0
- package/dist/server/retention/sweeper.js +289 -0
- package/dist/server/retention/target.d.ts +143 -0
- package/dist/server/retention/target.js +438 -0
- package/dist/server/search/index.d.ts +2 -0
- package/dist/server/search/index.js +6 -0
- package/dist/server/search/pgSearchProvider.d.ts +91 -0
- package/dist/server/search/pgSearchProvider.js +330 -0
- package/dist/server/search/pgSearchProvider.pg.test.d.ts +1 -0
- package/dist/server/search/pgSearchProvider.pg.test.js +338 -0
- package/dist/server/search/pgSearchSchema.d.ts +81 -0
- package/dist/server/search/pgSearchSchema.js +89 -0
- package/dist/server/sharing/index.d.ts +18 -0
- package/dist/server/sharing/index.js +16 -0
- package/dist/server/sharing/migrations.d.ts +6 -0
- package/dist/server/sharing/migrations.js +79 -0
- package/dist/server/sharing/router.d.ts +111 -0
- package/dist/server/sharing/router.js +93 -0
- package/dist/server/sharing/sql.d.ts +67 -0
- package/dist/server/sharing/sql.js +88 -0
- package/dist/server/sharing/store.d.ts +173 -0
- package/dist/server/sharing/store.js +484 -0
- package/dist/server/sqlite/backup.d.ts +48 -0
- package/dist/server/sqlite/backup.js +97 -0
- package/dist/server/sqlite/browse.d.ts +42 -0
- package/dist/server/sqlite/browse.js +84 -0
- package/dist/server/sqlite/export.d.ts +32 -0
- package/dist/server/sqlite/export.js +42 -0
- package/dist/server/sqlite/fts5.d.ts +106 -0
- package/dist/server/sqlite/fts5.js +154 -0
- package/dist/server/sqlite/health.d.ts +44 -0
- package/dist/server/sqlite/health.js +52 -0
- package/dist/server/sqlite/import.d.ts +36 -0
- package/dist/server/sqlite/import.js +108 -0
- package/dist/server/sqlite/index.d.ts +9 -0
- package/dist/server/sqlite/index.js +15 -0
- package/dist/server/sqlite/readonlySql.d.ts +15 -0
- package/dist/server/sqlite/readonlySql.js +65 -0
- package/dist/server/sqlite/stats.d.ts +22 -0
- package/dist/server/sqlite/stats.js +32 -0
- package/dist/server/sqlite/viewer.d.ts +17 -0
- package/dist/server/sqlite/viewer.js +56 -0
- package/dist/server/storage/binaryServerProcessing.d.ts +23 -0
- package/dist/server/storage/binaryServerProcessing.js +81 -0
- package/dist/server/storage/binaryServerStore.d.ts +3 -0
- package/dist/server/storage/binaryServerStore.js +95 -0
- package/dist/server/storage/diskStore.d.ts +2 -0
- package/dist/server/storage/diskStore.js +88 -0
- package/dist/server/storage/files/adapter.d.ts +33 -0
- package/dist/server/storage/files/adapter.js +203 -0
- package/dist/server/storage/files/catalogue.d.ts +72 -0
- package/dist/server/storage/files/catalogue.js +1033 -0
- package/dist/server/storage/files/chunkedUpload.d.ts +56 -0
- package/dist/server/storage/files/chunkedUpload.js +70 -0
- package/dist/server/storage/files/fileRoutes.d.ts +51 -0
- package/dist/server/storage/files/fileRoutes.js +405 -0
- package/dist/server/storage/files/index.d.ts +9 -0
- package/dist/server/storage/files/index.js +11 -0
- package/dist/server/storage/files/migrations.d.ts +2 -0
- package/dist/server/storage/files/migrations.js +163 -0
- package/dist/server/storage/files/mimeByExt.d.ts +1 -0
- package/dist/server/storage/files/mimeByExt.js +13 -0
- package/dist/server/storage/files/stack.d.ts +136 -0
- package/dist/server/storage/files/stack.js +132 -0
- package/dist/server/storage/files/storageOps.d.ts +54 -0
- package/dist/server/storage/files/storageOps.js +110 -0
- package/dist/server/storage/files/types.d.ts +261 -0
- package/dist/server/storage/files/types.js +22 -0
- package/dist/server/storage/imageThumbnail.d.ts +3 -0
- package/dist/server/storage/imageThumbnail.js +50 -0
- package/dist/server/storage/index.d.ts +15 -0
- package/dist/server/storage/index.js +27 -0
- package/dist/server/storage/mountMediaRoutes.d.ts +28 -0
- package/dist/server/storage/mountMediaRoutes.js +141 -0
- package/dist/server/storage/objectStoreMediaStore.d.ts +4 -0
- package/dist/server/storage/objectStoreMediaStore.js +57 -0
- package/dist/server/storage/s3ObjectStore.d.ts +63 -0
- package/dist/server/storage/s3ObjectStore.js +187 -0
- package/dist/server/storage/sources.d.ts +47 -0
- package/dist/server/storage/sources.js +148 -0
- package/dist/server/storage/streamRange.d.ts +19 -0
- package/dist/server/storage/streamRange.js +69 -0
- package/dist/server/storage/streamToken.d.ts +16 -0
- package/dist/server/storage/streamToken.js +54 -0
- package/dist/server/storage/transcodeJobs.d.ts +65 -0
- package/dist/server/storage/transcodeJobs.js +104 -0
- package/dist/server/storage/types.d.ts +193 -0
- package/dist/server/storage/types.js +41 -0
- package/dist/server/storage/videoDerivativeKeys.d.ts +127 -0
- package/dist/server/storage/videoDerivativeKeys.js +149 -0
- package/dist/server/storage/videoProcessor.d.ts +30 -0
- package/dist/server/storage/videoProcessor.js +148 -0
- package/dist/server/sync/alarm.d.ts +34 -0
- package/dist/server/sync/alarm.js +91 -0
- package/dist/server/sync/commands.d.ts +87 -0
- package/dist/server/sync/commands.js +241 -0
- package/dist/server/sync/engine.d.ts +62 -0
- package/dist/server/sync/engine.js +184 -0
- package/dist/server/sync/http.d.ts +106 -0
- package/dist/server/sync/http.js +243 -0
- package/dist/server/sync/index.d.ts +41 -0
- package/dist/server/sync/index.js +41 -0
- package/dist/server/sync/opLog.d.ts +61 -0
- package/dist/server/sync/opLog.js +96 -0
- package/dist/server/sync/planner.d.ts +31 -0
- package/dist/server/sync/planner.js +30 -0
- package/dist/server/sync/status.d.ts +63 -0
- package/dist/server/sync/status.js +49 -0
- package/dist/server/sync/timer.d.ts +52 -0
- package/dist/server/sync/timer.js +170 -0
- package/dist/server/sync/tokens.d.ts +25 -0
- package/dist/server/sync/tokens.js +51 -0
- package/dist/server/sync/types.d.ts +101 -0
- package/dist/server/sync/types.js +29 -0
- package/dist/server/ws/eventBus.d.ts +14 -0
- package/dist/server/ws/eventBus.js +42 -0
- package/dist/server/ws/hub.d.ts +20 -0
- package/dist/server/ws/hub.js +154 -0
- package/dist/server/ws/index.d.ts +5 -0
- package/dist/server/ws/index.js +7 -0
- package/dist/server/ws/presence.d.ts +11 -0
- package/dist/server/ws/presence.js +20 -0
- package/dist/server/ws/room.d.ts +17 -0
- package/dist/server/ws/room.js +23 -0
- package/dist/server/ws/types.d.ts +122 -0
- package/dist/server/ws/types.js +1 -0
- package/package.json +234 -0
- package/src/declaredDepsAreImported.spec.ts +87 -0
- package/src/leafSubpathsImportNothing.spec.ts +358 -0
- package/src/lockfileIsTracked.spec.ts +50 -0
- package/src/namedSubpathsResolve.spec.ts +122 -0
- package/src/noPathDeps.spec.ts +24 -0
- package/src/publishShape.spec.ts +89 -0
- package/src/server/activity/index.ts +59 -0
- package/src/server/activity/migrations.ts +67 -0
- package/src/server/activity/shareSource.ts +57 -0
- package/src/server/activity/store.spec.ts +255 -0
- package/src/server/activity/store.ts +368 -0
- package/src/server/app.spec.ts +243 -0
- package/src/server/app.ts +89 -0
- package/src/server/audit/index.ts +2 -0
- package/src/server/audit/migrations.ts +88 -0
- package/src/server/audit/sqliteSink.test.ts +200 -0
- package/src/server/audit/sqliteSink.ts +162 -0
- package/src/server/auth/cookie.spec.ts +46 -0
- package/src/server/auth/cookie.ts +42 -0
- package/src/server/auth/csrf.spec.ts +81 -0
- package/src/server/auth/csrf.ts +83 -0
- package/src/server/auth/fileTokenSigner.ts +40 -0
- package/src/server/auth/getOnly.spec.ts +53 -0
- package/src/server/auth/getOnly.ts +32 -0
- package/src/server/auth/index.ts +51 -0
- package/src/server/auth/jwt.spec.ts +106 -0
- package/src/server/auth/jwt.ts +75 -0
- package/src/server/auth/loginThrottle.spec.ts +213 -0
- package/src/server/auth/loginThrottle.ts +261 -0
- package/src/server/auth/middleware.spec.ts +148 -0
- package/src/server/auth/middleware.ts +56 -0
- package/src/server/auth/password.spec.ts +48 -0
- package/src/server/auth/password.ts +71 -0
- package/src/server/auth/sessionStore.spec.ts +189 -0
- package/src/server/auth/sessionStore.ts +261 -0
- package/src/server/auth/totp.spec.ts +73 -0
- package/src/server/auth/totp.ts +92 -0
- package/src/server/auth/types.ts +146 -0
- package/src/server/auth/vault.spec.ts +51 -0
- package/src/server/auth/vault.ts +71 -0
- package/src/server/context.ts +37 -0
- package/src/server/ctgr.spec.ts +138 -0
- package/src/server/ctgr.ts +108 -0
- package/src/server/db/kysely.spec.ts +62 -0
- package/src/server/db/kysely.ts +21 -0
- package/src/server/db/plumbingTypes.ts +126 -0
- package/src/server/dropzone/duplicates.ts +121 -0
- package/src/server/dropzone/index.ts +30 -0
- package/src/server/dropzone/scan.spec.ts +255 -0
- package/src/server/dropzone/scan.ts +342 -0
- package/src/server/errors.spec.ts +76 -0
- package/src/server/errors.ts +22 -0
- package/src/server/guard/argon2Binding.spec.ts +117 -0
- package/src/server/guard/argon2Binding.ts +195 -0
- package/src/server/guard/argon2Kat.spec.ts +250 -0
- package/src/server/guard/guard.spec.ts +390 -0
- package/src/server/guard/index.ts +6 -0
- package/src/server/guard/kdfWorker.spec.ts +283 -0
- package/src/server/guard/kdfWorker.ts +367 -0
- package/src/server/guard/kdfWorkerEntry.ts +16 -0
- package/src/server/guard/passwordVerifier.spec.ts +71 -0
- package/src/server/guard/passwordVerifier.ts +175 -0
- package/src/server/guard/revocationStore.spec.ts +135 -0
- package/src/server/guard/revocationStore.ts +291 -0
- package/src/server/guard/sessionSigner.ts +425 -0
- package/src/server/guard/verifier.ts +27 -0
- package/src/server/incidents/channels.ts +487 -0
- package/src/server/incidents/checks.ts +120 -0
- package/src/server/incidents/incidents.spec.ts +463 -0
- package/src/server/incidents/index.ts +96 -0
- package/src/server/incidents/migrations.ts +66 -0
- package/src/server/incidents/notifier.ts +215 -0
- package/src/server/incidents/reporter.ts +110 -0
- package/src/server/incidents/routes.ts +314 -0
- package/src/server/incidents/store.ts +329 -0
- package/src/server/incidents/types.ts +116 -0
- package/src/server/index.ts +248 -0
- package/src/server/jobs/fireQueue.spec.ts +85 -0
- package/src/server/jobs/fireQueue.ts +37 -0
- package/src/server/jobs/index.ts +26 -0
- package/src/server/jobs/logger.ts +16 -0
- package/src/server/jobs/plainjobAdapter.spec.ts +34 -0
- package/src/server/jobs/plainjobAdapter.ts +54 -0
- package/src/server/jobs/polling.spec.ts +219 -0
- package/src/server/jobs/polling.ts +81 -0
- package/src/server/jobs/runner.spec.ts +213 -0
- package/src/server/jobs/runner.ts +213 -0
- package/src/server/jobs/timeout.spec.ts +35 -0
- package/src/server/jobs/timeout.ts +53 -0
- package/src/server/jobs/types.ts +188 -0
- package/src/server/master-lock/guard.spec.ts +347 -0
- package/src/server/master-lock/guard.ts +290 -0
- package/src/server/master-lock/index.ts +44 -0
- package/src/server/master-lock/lockPage.spec.ts +88 -0
- package/src/server/master-lock/lockPage.ts +313 -0
- package/src/server/master-lock/masterLock.spec.ts +338 -0
- package/src/server/master-lock/masterLock.ts +627 -0
- package/src/server/master-lock/principals.spec.ts +378 -0
- package/src/server/master-lock/principals.ts +175 -0
- package/src/server/master-lock/seed.spec.ts +69 -0
- package/src/server/master-lock/seed.ts +112 -0
- package/src/server/master-lock/store.ts +35 -0
- package/src/server/media-bun/image.spec.ts +115 -0
- package/src/server/media-bun/image.ts +134 -0
- package/src/server/media-bun/index.ts +52 -0
- package/src/server/media-bun/streaming.spec.ts +134 -0
- package/src/server/media-bun/streaming.ts +347 -0
- package/src/server/media-bun/video.spec.ts +115 -0
- package/src/server/media-bun/video.ts +232 -0
- package/src/server/metrics/metricsBuffer.ts +92 -0
- package/src/server/metrics/normalizeRoute.spec.ts +51 -0
- package/src/server/metrics/normalizeRoute.ts +63 -0
- package/src/server/middleware/correlationId.ts +24 -0
- package/src/server/middleware/corsAllowlist.spec.ts +118 -0
- package/src/server/middleware/corsAllowlist.ts +69 -0
- package/src/server/middleware/errorEnvelope.ts +38 -0
- package/src/server/middleware/index.ts +19 -0
- package/src/server/middleware/localOrigin.spec.ts +168 -0
- package/src/server/middleware/localOrigin.ts +139 -0
- package/src/server/middleware/rateLimit.spec.ts +72 -0
- package/src/server/middleware/rateLimit.ts +125 -0
- package/src/server/middleware/requestLogger.ts +95 -0
- package/src/server/middleware/securityHeaders.ts +78 -0
- package/src/server/middleware/zValidator.spec.ts +251 -0
- package/src/server/middleware/zValidator.ts +114 -0
- package/src/server/migration/runner.spec.ts +88 -0
- package/src/server/migration/runner.ts +87 -0
- package/src/server/migration/types.ts +17 -0
- package/src/server/net/outboundGuard.spec.ts +90 -0
- package/src/server/net/outboundGuard.ts +205 -0
- package/src/server/notifications/hub.ts +245 -0
- package/src/server/notifications/index.ts +77 -0
- package/src/server/notifications/migrations.ts +39 -0
- package/src/server/notifications/routes.ts +177 -0
- package/src/server/notifications/service.spec.ts +305 -0
- package/src/server/notifications/service.ts +112 -0
- package/src/server/paths/appDataHome.spec.ts +87 -0
- package/src/server/paths/appDataHome.ts +84 -0
- package/src/server/private-media/blobRoute.spec.ts +165 -0
- package/src/server/private-media/crypto.spec.ts +81 -0
- package/src/server/private-media/crypto.ts +155 -0
- package/src/server/private-media/gatePolicy.spec.ts +98 -0
- package/src/server/private-media/index-store.ts +83 -0
- package/src/server/private-media/mountPrivateRoutes.ts +265 -0
- package/src/server/private-media/pipeline.spec.ts +111 -0
- package/src/server/private-media/pipeline.ts +170 -0
- package/src/server/private-media/routes.spec.ts +265 -0
- package/src/server/private-media/unlock.ts +112 -0
- package/src/server/resume/index.ts +8 -0
- package/src/server/resume/resume.spec.ts +118 -0
- package/src/server/resume/routes.ts +92 -0
- package/src/server/resume/store.ts +82 -0
- package/src/server/retention/archive.spec.ts +181 -0
- package/src/server/retention/archive.ts +353 -0
- package/src/server/retention/fleet.ts +104 -0
- package/src/server/retention/index.ts +101 -0
- package/src/server/retention/policy.spec.ts +132 -0
- package/src/server/retention/policy.ts +392 -0
- package/src/server/retention/store.spec.ts +236 -0
- package/src/server/retention/store.ts +338 -0
- package/src/server/retention/sweeper.spec.ts +321 -0
- package/src/server/retention/sweeper.ts +367 -0
- package/src/server/retention/target.spec.ts +592 -0
- package/src/server/retention/target.ts +613 -0
- package/src/server/search/index.ts +12 -0
- package/src/server/search/pgSearchProvider.pg.test.ts +392 -0
- package/src/server/search/pgSearchProvider.ts +421 -0
- package/src/server/search/pgSearchSchema.ts +147 -0
- package/src/server/sharing/index.ts +65 -0
- package/src/server/sharing/migrations.ts +128 -0
- package/src/server/sharing/router.spec.ts +167 -0
- package/src/server/sharing/router.ts +194 -0
- package/src/server/sharing/sql.ts +109 -0
- package/src/server/sharing/store.spec.ts +408 -0
- package/src/server/sharing/store.ts +737 -0
- package/src/server/sqlite/backup.spec.ts +114 -0
- package/src/server/sqlite/backup.ts +154 -0
- package/src/server/sqlite/browse.spec.ts +70 -0
- package/src/server/sqlite/browse.ts +120 -0
- package/src/server/sqlite/export.spec.ts +43 -0
- package/src/server/sqlite/export.ts +74 -0
- package/src/server/sqlite/fts5.spec.ts +175 -0
- package/src/server/sqlite/fts5.ts +255 -0
- package/src/server/sqlite/health.spec.ts +79 -0
- package/src/server/sqlite/health.ts +105 -0
- package/src/server/sqlite/import.spec.ts +75 -0
- package/src/server/sqlite/import.ts +141 -0
- package/src/server/sqlite/index.ts +64 -0
- package/src/server/sqlite/readonlySql.spec.ts +58 -0
- package/src/server/sqlite/readonlySql.ts +67 -0
- package/src/server/sqlite/stats.spec.ts +21 -0
- package/src/server/sqlite/stats.ts +61 -0
- package/src/server/sqlite/viewer.spec.ts +127 -0
- package/src/server/sqlite/viewer.ts +72 -0
- package/src/server/storage/binaryServerProcessing.integration.spec.ts +222 -0
- package/src/server/storage/binaryServerProcessing.spec.ts +150 -0
- package/src/server/storage/binaryServerProcessing.ts +127 -0
- package/src/server/storage/binaryServerStore.ts +102 -0
- package/src/server/storage/chunked.integration.spec.ts +185 -0
- package/src/server/storage/diskStore.ts +83 -0
- package/src/server/storage/files/adapter.photo.display.spec.ts +169 -0
- package/src/server/storage/files/adapter.photo.spec.ts +128 -0
- package/src/server/storage/files/adapter.spec.ts +121 -0
- package/src/server/storage/files/adapter.ts +255 -0
- package/src/server/storage/files/catalogue.media-host.spec.ts +184 -0
- package/src/server/storage/files/catalogue.orderings.spec.ts +157 -0
- package/src/server/storage/files/catalogue.private.spec.ts +106 -0
- package/src/server/storage/files/catalogue.recover.spec.ts +131 -0
- package/src/server/storage/files/catalogue.sort.spec.ts +154 -0
- package/src/server/storage/files/catalogue.spec.ts +133 -0
- package/src/server/storage/files/catalogue.trash-import.spec.ts +125 -0
- package/src/server/storage/files/catalogue.ts +1326 -0
- package/src/server/storage/files/chunkedUpload.spec.ts +111 -0
- package/src/server/storage/files/chunkedUpload.ts +146 -0
- package/src/server/storage/files/collections.spec.ts +294 -0
- package/src/server/storage/files/fileRoutes.replace.spec.ts +88 -0
- package/src/server/storage/files/fileRoutes.spec.ts +327 -0
- package/src/server/storage/files/fileRoutes.ts +462 -0
- package/src/server/storage/files/index.ts +66 -0
- package/src/server/storage/files/migrations.ts +187 -0
- package/src/server/storage/files/mimeByExt.ts +14 -0
- package/src/server/storage/files/stack.spec.ts +258 -0
- package/src/server/storage/files/stack.ts +284 -0
- package/src/server/storage/files/storageOps.spec.ts +126 -0
- package/src/server/storage/files/storageOps.ts +213 -0
- package/src/server/storage/files/types.ts +285 -0
- package/src/server/storage/imageThumbnail.spec.ts +81 -0
- package/src/server/storage/imageThumbnail.ts +60 -0
- package/src/server/storage/index.ts +122 -0
- package/src/server/storage/mountMediaRoutes.ts +231 -0
- package/src/server/storage/objectStore.spec.ts +272 -0
- package/src/server/storage/objectStoreMediaStore.ts +63 -0
- package/src/server/storage/s3ObjectStore.ts +239 -0
- package/src/server/storage/sources.ts +245 -0
- package/src/server/storage/storage.spec.ts +219 -0
- package/src/server/storage/streamRange.ts +92 -0
- package/src/server/storage/streamToken.ts +76 -0
- package/src/server/storage/transcode.integration.spec.ts +134 -0
- package/src/server/storage/transcodeJobs.spec.ts +152 -0
- package/src/server/storage/transcodeJobs.ts +171 -0
- package/src/server/storage/types.ts +230 -0
- package/src/server/storage/videoDerivativeKeys.spec.ts +112 -0
- package/src/server/storage/videoDerivativeKeys.ts +172 -0
- package/src/server/storage/videoProcessor.posterOnly.spec.ts +106 -0
- package/src/server/storage/videoProcessor.ts +229 -0
- package/src/server/sync/alarm.spec.ts +149 -0
- package/src/server/sync/alarm.ts +137 -0
- package/src/server/sync/commands.spec.ts +145 -0
- package/src/server/sync/commands.ts +361 -0
- package/src/server/sync/engine.spec.ts +496 -0
- package/src/server/sync/engine.ts +255 -0
- package/src/server/sync/http.ts +316 -0
- package/src/server/sync/httpRemote.spec.ts +158 -0
- package/src/server/sync/index.ts +90 -0
- package/src/server/sync/opLog.spec.ts +110 -0
- package/src/server/sync/opLog.ts +189 -0
- package/src/server/sync/planner.spec.ts +53 -0
- package/src/server/sync/planner.ts +62 -0
- package/src/server/sync/status.spec.ts +92 -0
- package/src/server/sync/status.ts +108 -0
- package/src/server/sync/timer.spec.ts +150 -0
- package/src/server/sync/timer.ts +207 -0
- package/src/server/sync/tokens.spec.ts +38 -0
- package/src/server/sync/tokens.ts +94 -0
- package/src/server/sync/types.ts +108 -0
- package/src/server/ws/eventBus.spec.ts +67 -0
- package/src/server/ws/eventBus.ts +46 -0
- package/src/server/ws/hub.integration.spec.ts +93 -0
- package/src/server/ws/hub.spec.ts +282 -0
- package/src/server/ws/hub.ts +168 -0
- package/src/server/ws/index.ts +20 -0
- package/src/server/ws/presence.spec.ts +27 -0
- package/src/server/ws/presence.ts +26 -0
- package/src/server/ws/room.spec.ts +36 -0
- package/src/server/ws/room.ts +29 -0
- package/src/server/ws/types.ts +134 -0
- package/src/skippedTestsAreAnswered.spec.ts +215 -0
- package/src/testTmpRoot.spec.ts +102 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
// Pure ffmpeg/ffprobe argument builders + parsers for the STREAMING pipeline: faststart
|
|
2
|
+
// remux, the HLS adaptive-bitrate ladder, scrub-preview sprite sheets + WebVTT, poster
|
|
3
|
+
// frames, and a codec-aware probe. No Bun.spawn, no I/O — every argv + parser here is unit
|
|
4
|
+
// tested without ffmpeg present; `video.ts` is the thin spawn shell that runs them
|
|
5
|
+
// (probeVideoStream / runFfmpegProgress / extractFrameJpeg).
|
|
6
|
+
//
|
|
7
|
+
// Companion to `ffmpeg.ts` (which owns the trim/thumbnail builders + basic probe): these
|
|
8
|
+
// are the additional pieces a streaming server (cursedalchemy's media pipeline, the
|
|
9
|
+
// binary-server) needs to turn an upload into web-seekable, adaptively-streamable output.
|
|
10
|
+
//
|
|
11
|
+
// ffmpeg/ffprobe are expected on PATH (or via FFMPEG_PATH/FFPROBE_PATH) — never an npm dep.
|
|
12
|
+
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
// ── Web-safe encode primitives (one source of truth for "decodes everywhere") ──────────
|
|
16
|
+
// libx264 defaults to HIGH profile, whose CABAC/8x8-transform features some hardware
|
|
17
|
+
// decoders mis-handle (solid GREEN frame); Main profile drops those at negligible cost and
|
|
18
|
+
// is universally supported. yuv420p is the only chroma layout browsers decode reliably.
|
|
19
|
+
export const h264WebVideoArgs = (crf: string): string[] => [
|
|
20
|
+
'-c:v',
|
|
21
|
+
'libx264',
|
|
22
|
+
'-preset',
|
|
23
|
+
'veryfast',
|
|
24
|
+
'-crf',
|
|
25
|
+
crf,
|
|
26
|
+
'-profile:v',
|
|
27
|
+
'main',
|
|
28
|
+
'-pix_fmt',
|
|
29
|
+
'yuv420p',
|
|
30
|
+
];
|
|
31
|
+
export const aacWebAudioArgs = (bitrate: string): string[] => ['-c:a', 'aac', '-b:a', bitrate];
|
|
32
|
+
/** Move the moov atom to the front so playback can start before the full download. */
|
|
33
|
+
export const FASTSTART = ['-movflags', '+faststart'];
|
|
34
|
+
/** Cap the long edge at ~1080p, keep aspect, force even dimensions (yuv420p needs even). */
|
|
35
|
+
export const LIBRARY_SCALE = "scale='min(1920,iw)':'-2'";
|
|
36
|
+
/** Higher quality than ephemeral Moments (28) — this output is retained. */
|
|
37
|
+
export const LIBRARY_CRF = '23';
|
|
38
|
+
|
|
39
|
+
// ── Codec-aware probe (richer than ffmpeg.ts's ProbeResult: adds codecs/pixfmt/audio so the
|
|
40
|
+
// pipeline can DECIDE whether to stream-copy) ────────────────────────────────────────
|
|
41
|
+
export interface VideoProbe {
|
|
42
|
+
duration: number;
|
|
43
|
+
width: number;
|
|
44
|
+
height: number;
|
|
45
|
+
vcodec: string;
|
|
46
|
+
acodec: string;
|
|
47
|
+
pixfmt: string;
|
|
48
|
+
hasAudio: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const EMPTY_PROBE: VideoProbe = {
|
|
52
|
+
duration: 0,
|
|
53
|
+
width: 0,
|
|
54
|
+
height: 0,
|
|
55
|
+
vcodec: '',
|
|
56
|
+
acodec: '',
|
|
57
|
+
pixfmt: '',
|
|
58
|
+
hasAudio: false,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** ffprobe argv: one call for everything the pipeline decides on (codecs + dims + duration). */
|
|
62
|
+
export const buildStreamProbeArgs = (filePath: string): string[] => [
|
|
63
|
+
'-v',
|
|
64
|
+
'error',
|
|
65
|
+
'-show_entries',
|
|
66
|
+
'format=duration:stream=index,codec_type,codec_name,width,height,pix_fmt',
|
|
67
|
+
'-of',
|
|
68
|
+
'json',
|
|
69
|
+
filePath,
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
interface StreamProbeJson {
|
|
73
|
+
format?: { duration?: string };
|
|
74
|
+
streams?: Array<{
|
|
75
|
+
codec_type?: string;
|
|
76
|
+
codec_name?: string;
|
|
77
|
+
width?: number;
|
|
78
|
+
height?: number;
|
|
79
|
+
pix_fmt?: string;
|
|
80
|
+
}>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Parse the `buildStreamProbeArgs` JSON into a {@link VideoProbe}. */
|
|
84
|
+
export const parseStreamProbe = (json: StreamProbeJson): VideoProbe => {
|
|
85
|
+
const streams = Array.isArray(json?.streams) ? json.streams : [];
|
|
86
|
+
const v = streams.find((s) => s.codec_type === 'video') ?? {};
|
|
87
|
+
const a = streams.find((s) => s.codec_type === 'audio');
|
|
88
|
+
return {
|
|
89
|
+
duration: Number(json?.format?.duration) || 0,
|
|
90
|
+
width: Number(v.width) || 0,
|
|
91
|
+
height: Number(v.height) || 0,
|
|
92
|
+
vcodec: String(v.codec_name ?? ''),
|
|
93
|
+
acodec: String(a?.codec_name ?? ''),
|
|
94
|
+
pixfmt: String(v.pix_fmt ?? ''),
|
|
95
|
+
hasAudio: Boolean(a),
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// ── Faststart: stream-copy a source that's already web-safe (fast, lossless — just move the
|
|
100
|
+
// moov atom), else re-encode to the web-safe ladder. ─────────────────────────────────
|
|
101
|
+
export const canStreamCopy = (p: VideoProbe, maxWidth = 1920): boolean =>
|
|
102
|
+
p.vcodec === 'h264' &&
|
|
103
|
+
p.pixfmt === 'yuv420p' &&
|
|
104
|
+
(p.acodec === '' || p.acodec === 'aac') &&
|
|
105
|
+
p.width > 0 &&
|
|
106
|
+
p.width <= maxWidth;
|
|
107
|
+
|
|
108
|
+
export const faststartArgs = (
|
|
109
|
+
inPath: string,
|
|
110
|
+
outPath: string,
|
|
111
|
+
opts: { copy: boolean; crf?: number },
|
|
112
|
+
): string[] => {
|
|
113
|
+
if (opts.copy) return ['-i', inPath, '-c', 'copy', ...FASTSTART, outPath];
|
|
114
|
+
const crf = String(opts.crf ?? Number(LIBRARY_CRF));
|
|
115
|
+
return [
|
|
116
|
+
'-i',
|
|
117
|
+
inPath,
|
|
118
|
+
'-vf',
|
|
119
|
+
LIBRARY_SCALE,
|
|
120
|
+
...h264WebVideoArgs(crf),
|
|
121
|
+
...aacWebAudioArgs('160k'),
|
|
122
|
+
...FASTSTART,
|
|
123
|
+
outPath,
|
|
124
|
+
];
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Parse one ffmpeg `-progress pipe:1` line into elapsed microseconds, or null if the line
|
|
129
|
+
* carries no out_time. `runFfmpegProgress` turns a stream of these into a 0..99 percent.
|
|
130
|
+
*/
|
|
131
|
+
export const parseProgressMicros = (line: string): number | null => {
|
|
132
|
+
const m = /^out_time_us=(\d+)/.exec(line) ?? /^out_time_ms=(\d+)/.exec(line);
|
|
133
|
+
if (!m) return null;
|
|
134
|
+
return line.startsWith('out_time_ms') ? Number(m[1]) * 1000 : Number(m[1]);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// ── HLS adaptive-bitrate ladder ─────────────────────────────────────────────────────────
|
|
138
|
+
// Per-rung video bitrate cap + buffer + audio rate. One source of truth for rung tuning.
|
|
139
|
+
const LADDER_PRESETS: Record<number, { maxrate: string; bufsize: string; abr: string }> = {
|
|
140
|
+
360: { maxrate: '856k', bufsize: '1200k', abr: '96k' },
|
|
141
|
+
480: { maxrate: '1400k', bufsize: '2100k', abr: '128k' },
|
|
142
|
+
720: { maxrate: '2800k', bufsize: '4200k', abr: '128k' },
|
|
143
|
+
1080: { maxrate: '5000k', bufsize: '7500k', abr: '192k' },
|
|
144
|
+
1440: { maxrate: '8000k', bufsize: '12000k', abr: '192k' },
|
|
145
|
+
2160: { maxrate: '16000k', bufsize: '24000k', abr: '192k' },
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/** Drop unknown/too-tall rungs (never upscale); always keep at least the source-or-720 rung. */
|
|
149
|
+
export const resolveLadder = (heights: number[], sourceHeight: number): number[] => {
|
|
150
|
+
const valid = heights
|
|
151
|
+
.filter((h) => LADDER_PRESETS[h])
|
|
152
|
+
.filter((h) => sourceHeight <= 0 || h <= sourceHeight);
|
|
153
|
+
const ladder = valid.length ? valid : [Math.min(720, sourceHeight || 720)];
|
|
154
|
+
return [...new Set(ladder)].sort((a, b) => a - b);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Build the HLS multi-rendition transcode. `outDir/<height>/` subdirs must already exist.
|
|
159
|
+
* Emits master.m3u8 + per-rung playlist.m3u8 + mpegts segments, variants NAMED by height
|
|
160
|
+
* so storage keys read `…/720/seg_00001.ts` rather than an opaque index.
|
|
161
|
+
*/
|
|
162
|
+
export const hlsLadderArgs = (
|
|
163
|
+
inPath: string,
|
|
164
|
+
outDir: string,
|
|
165
|
+
opts: {
|
|
166
|
+
heights: number[];
|
|
167
|
+
crf: number;
|
|
168
|
+
hasAudio: boolean;
|
|
169
|
+
// When set, ffmpeg AES-128-encrypts every segment using this key-info file (line 1 = the
|
|
170
|
+
// key URI written into the playlist's #EXT-X-KEY, line 2 = the raw 16-byte key file, line 3
|
|
171
|
+
// = IV hex). Yields encrypted-at-rest, edge-cacheable segments the player decrypts. Used by
|
|
172
|
+
// the private-media pipeline; omit for public (plaintext) media.
|
|
173
|
+
keyInfoFile?: string;
|
|
174
|
+
},
|
|
175
|
+
): string[] => {
|
|
176
|
+
const hs = opts.heights.length ? opts.heights : [720];
|
|
177
|
+
const n = hs.length;
|
|
178
|
+
const filter =
|
|
179
|
+
`[0:v]split=${n}${hs.map((_, i) => `[v${i}]`).join('')};` +
|
|
180
|
+
hs.map((h, i) => `[v${i}]scale=-2:${h}[vo${i}]`).join(';');
|
|
181
|
+
|
|
182
|
+
const args: string[] = ['-i', inPath, '-filter_complex', filter];
|
|
183
|
+
hs.forEach((h, i) => {
|
|
184
|
+
const p = LADDER_PRESETS[h] ?? LADDER_PRESETS[720];
|
|
185
|
+
args.push(
|
|
186
|
+
'-map',
|
|
187
|
+
`[vo${i}]`,
|
|
188
|
+
`-c:v:${i}`,
|
|
189
|
+
'libx264',
|
|
190
|
+
'-preset',
|
|
191
|
+
'veryfast',
|
|
192
|
+
`-crf:v:${i}`,
|
|
193
|
+
String(opts.crf),
|
|
194
|
+
`-profile:v:${i}`,
|
|
195
|
+
'main',
|
|
196
|
+
`-maxrate:v:${i}`,
|
|
197
|
+
p.maxrate,
|
|
198
|
+
`-bufsize:v:${i}`,
|
|
199
|
+
p.bufsize,
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
args.push('-pix_fmt', 'yuv420p');
|
|
203
|
+
if (opts.hasAudio) {
|
|
204
|
+
hs.forEach((h, i) => {
|
|
205
|
+
const p = LADDER_PRESETS[h] ?? LADDER_PRESETS[720];
|
|
206
|
+
args.push('-map', 'a:0', `-c:a:${i}`, 'aac', `-b:a:${i}`, p.abr, '-ac', '2');
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
const varMap = hs
|
|
210
|
+
.map((h, i) => (opts.hasAudio ? `v:${i},a:${i},name:${h}` : `v:${i},name:${h}`))
|
|
211
|
+
.join(' ');
|
|
212
|
+
args.push(
|
|
213
|
+
'-f',
|
|
214
|
+
'hls',
|
|
215
|
+
'-hls_time',
|
|
216
|
+
'4',
|
|
217
|
+
'-hls_playlist_type',
|
|
218
|
+
'vod',
|
|
219
|
+
'-hls_segment_type',
|
|
220
|
+
'mpegts',
|
|
221
|
+
...(opts.keyInfoFile ? ['-hls_key_info_file', opts.keyInfoFile] : []),
|
|
222
|
+
'-master_pl_name',
|
|
223
|
+
'master.m3u8',
|
|
224
|
+
'-hls_segment_filename',
|
|
225
|
+
path.join(outDir, '%v', 'seg_%05d.ts'),
|
|
226
|
+
'-var_stream_map',
|
|
227
|
+
varMap,
|
|
228
|
+
path.join(outDir, '%v', 'playlist.m3u8'),
|
|
229
|
+
);
|
|
230
|
+
return args;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/** Poster: a frame `atSeconds` in (default ~1s, to skip black intros), scaled to the library
|
|
234
|
+
* width, as JPEG. Pass a smaller seek (or 0) for very short clips — seeking past EOF yields no
|
|
235
|
+
* frame and the extract fails, so callers that know the duration should clamp `atSeconds`. */
|
|
236
|
+
export const posterArgs = (inPath: string, outPath: string, atSeconds = 1): string[] => [
|
|
237
|
+
'-ss',
|
|
238
|
+
String(Math.max(0, atSeconds)),
|
|
239
|
+
'-i',
|
|
240
|
+
inPath,
|
|
241
|
+
'-vframes',
|
|
242
|
+
'1',
|
|
243
|
+
'-vf',
|
|
244
|
+
LIBRARY_SCALE,
|
|
245
|
+
outPath,
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
/** Decode a still image to a width-capped JPEG thumbnail. The fallback for formats sharp's
|
|
249
|
+
* prebuilt binary can't decode — notably HEIC/HEVC (iPhone photos), which the licensed-out
|
|
250
|
+
* libheif build rejects but ffmpeg's hevc decoder handles. `-update 1` writes the single frame
|
|
251
|
+
* to a fixed filename (no image-sequence pattern); height rounds to even (`-2`) for the encoder. */
|
|
252
|
+
export const imageThumbnailArgs = (inPath: string, outPath: string, maxWidth = 512): string[] => [
|
|
253
|
+
'-i',
|
|
254
|
+
inPath,
|
|
255
|
+
'-vf',
|
|
256
|
+
`scale='min(${maxWidth},iw)':-2`,
|
|
257
|
+
'-frames:v',
|
|
258
|
+
'1',
|
|
259
|
+
'-update',
|
|
260
|
+
'1',
|
|
261
|
+
outPath,
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
/** ffmpeg argv to grab one scaled JPEG frame at `atSeconds` on stdout (`-ss` before `-i`
|
|
265
|
+
* fast-seeks — instant on a faststart file). The spawn runner reads pipe:1. */
|
|
266
|
+
export const buildFrameJpegArgs = (
|
|
267
|
+
filePath: string,
|
|
268
|
+
atSeconds: number,
|
|
269
|
+
maxWidth = 240,
|
|
270
|
+
): string[] => [
|
|
271
|
+
'-ss',
|
|
272
|
+
String(Math.max(0, atSeconds)),
|
|
273
|
+
'-i',
|
|
274
|
+
filePath,
|
|
275
|
+
'-frames:v',
|
|
276
|
+
'1',
|
|
277
|
+
'-vf',
|
|
278
|
+
`scale=${maxWidth}:-2`,
|
|
279
|
+
'-f',
|
|
280
|
+
'mjpeg',
|
|
281
|
+
'pipe:1',
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
// ── Scrub-preview sprite sheet + WebVTT (YouTube-style hover/drag thumbnails) ───────────
|
|
285
|
+
const SPRITE_THUMB_WIDTH = 160;
|
|
286
|
+
const SPRITE_MAX_TILES = 144; // a single ~12×12 sheet; widen the interval past this
|
|
287
|
+
|
|
288
|
+
export interface SpritePlan {
|
|
289
|
+
effInterval: number; // seconds between sampled frames (widened for long videos)
|
|
290
|
+
count: number;
|
|
291
|
+
cols: number;
|
|
292
|
+
rows: number;
|
|
293
|
+
thumbW: number;
|
|
294
|
+
thumbH: number;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Sheet geometry: sample every `interval`s, widening it so a long video still fits one sheet. */
|
|
298
|
+
export const planSprite = (
|
|
299
|
+
duration: number,
|
|
300
|
+
interval: number,
|
|
301
|
+
srcW: number,
|
|
302
|
+
srcH: number,
|
|
303
|
+
): SpritePlan => {
|
|
304
|
+
const dur = Math.max(1, duration);
|
|
305
|
+
const effInterval = Math.max(1, interval, Math.ceil(dur / SPRITE_MAX_TILES));
|
|
306
|
+
const count = Math.max(1, Math.min(SPRITE_MAX_TILES, Math.ceil(dur / effInterval)));
|
|
307
|
+
const cols = Math.ceil(Math.sqrt(count));
|
|
308
|
+
const rows = Math.ceil(count / cols);
|
|
309
|
+
const thumbW = SPRITE_THUMB_WIDTH;
|
|
310
|
+
const thumbH = srcW > 0 ? Math.max(2, Math.round((thumbW * srcH) / srcW / 2) * 2) : 90;
|
|
311
|
+
return { effInterval, count, cols, rows, thumbW, thumbH };
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
export const spriteArgs = (inPath: string, outPath: string, plan: SpritePlan): string[] => [
|
|
315
|
+
'-i',
|
|
316
|
+
inPath,
|
|
317
|
+
'-vf',
|
|
318
|
+
`fps=1/${plan.effInterval},scale=${plan.thumbW}:-2,tile=${plan.cols}x${plan.rows}`,
|
|
319
|
+
'-frames:v',
|
|
320
|
+
'1',
|
|
321
|
+
'-q:v',
|
|
322
|
+
'5',
|
|
323
|
+
outPath,
|
|
324
|
+
];
|
|
325
|
+
|
|
326
|
+
const vttTime = (s: number): string => {
|
|
327
|
+
const h = Math.floor(s / 3600);
|
|
328
|
+
const m = Math.floor((s % 3600) / 60);
|
|
329
|
+
const sec = s % 60;
|
|
330
|
+
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${sec.toFixed(3).padStart(6, '0')}`;
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* WebVTT thumbnail track: one cue per tile pointing at a `#xywh` media fragment of the sheet.
|
|
335
|
+
* The sprite URL is relative (default "sprites.jpg") so it resolves against the VTT's own route.
|
|
336
|
+
*/
|
|
337
|
+
export const buildSpriteVtt = (plan: SpritePlan, spriteName = 'sprites.jpg'): string => {
|
|
338
|
+
let out = 'WEBVTT\n\n';
|
|
339
|
+
for (let i = 0; i < plan.count; i++) {
|
|
340
|
+
const start = i * plan.effInterval;
|
|
341
|
+
const end = (i + 1) * plan.effInterval;
|
|
342
|
+
const x = (i % plan.cols) * plan.thumbW;
|
|
343
|
+
const y = Math.floor(i / plan.cols) * plan.thumbH;
|
|
344
|
+
out += `${vttTime(start)} --> ${vttTime(end)}\n${spriteName}#xywh=${x},${y},${plan.thumbW},${plan.thumbH}\n\n`;
|
|
345
|
+
}
|
|
346
|
+
return out;
|
|
347
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import type { VideoProgressEvent } from 'cursedbelt-core/media';
|
|
6
|
+
import { extractThumbnail, isFfmpegAvailable, probeMedia, trimVideo } from './video';
|
|
7
|
+
|
|
8
|
+
// ffmpeg/ffprobe are required for these; skip cleanly if absent (CI without them).
|
|
9
|
+
const haveFfmpeg = await isFfmpegAvailable();
|
|
10
|
+
|
|
11
|
+
let dir = '';
|
|
12
|
+
let source = '';
|
|
13
|
+
|
|
14
|
+
/** Synthesize a 10s 320×240 test clip with ffmpeg's lavfi sources. */
|
|
15
|
+
async function makeSource(path: string): Promise<void> {
|
|
16
|
+
const proc = Bun.spawn(
|
|
17
|
+
[
|
|
18
|
+
'ffmpeg',
|
|
19
|
+
'-hide_banner',
|
|
20
|
+
'-y',
|
|
21
|
+
'-f',
|
|
22
|
+
'lavfi',
|
|
23
|
+
'-i',
|
|
24
|
+
'testsrc=duration=10:size=320x240:rate=15',
|
|
25
|
+
'-f',
|
|
26
|
+
'lavfi',
|
|
27
|
+
'-i',
|
|
28
|
+
'sine=frequency=440:duration=10',
|
|
29
|
+
'-pix_fmt',
|
|
30
|
+
'yuv420p',
|
|
31
|
+
'-c:v',
|
|
32
|
+
'libx264',
|
|
33
|
+
'-c:a',
|
|
34
|
+
'aac',
|
|
35
|
+
'-shortest',
|
|
36
|
+
path,
|
|
37
|
+
],
|
|
38
|
+
{ stdout: 'ignore', stderr: 'ignore' },
|
|
39
|
+
);
|
|
40
|
+
await proc.exited;
|
|
41
|
+
if (proc.exitCode !== 0) throw new Error('failed to synthesize test clip');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
beforeAll(async () => {
|
|
45
|
+
if (!haveFfmpeg) return;
|
|
46
|
+
dir = mkdtempSync(join(tmpdir(), 'cb-media-video-'));
|
|
47
|
+
source = join(dir, 'source.mp4');
|
|
48
|
+
await makeSource(source);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterAll(() => {
|
|
52
|
+
if (dir) rmSync(dir, { recursive: true, force: true });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// SKIPPED on a host without ffmpeg/ffprobe — it synthesizes and decodes real media. Runs by default wherever ffmpeg is installed.
|
|
56
|
+
describe.skipIf(!haveFfmpeg)('media-bun video (ffmpeg)', () => {
|
|
57
|
+
test('probeMedia reads dimensions + duration', async () => {
|
|
58
|
+
const meta = await probeMedia(source);
|
|
59
|
+
expect(meta.width).toBe(320);
|
|
60
|
+
expect(meta.height).toBe(240);
|
|
61
|
+
expect(meta.durationSeconds).toBeGreaterThan(9);
|
|
62
|
+
expect(meta.durationSeconds).toBeLessThan(11);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('trimVideo writes a ~6s clip and reports progress (acceptance #9)', async () => {
|
|
66
|
+
const out = join(dir, 'trim.mp4');
|
|
67
|
+
const events: VideoProgressEvent[] = [];
|
|
68
|
+
await trimVideo({ inputPath: source, outputPath: out, startSeconds: 2, endSeconds: 8 }, (e) =>
|
|
69
|
+
events.push(e),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
expect(await Bun.file(out).exists()).toBe(true);
|
|
73
|
+
const meta = await probeMedia(out);
|
|
74
|
+
expect(meta.durationSeconds).toBeGreaterThan(5.5);
|
|
75
|
+
expect(meta.durationSeconds).toBeLessThan(6.5);
|
|
76
|
+
|
|
77
|
+
// Saw at least one encode event and a terminal done at 100%.
|
|
78
|
+
expect(events.some((e) => e.phase === 'encode')).toBe(true);
|
|
79
|
+
expect(events.at(-1)).toEqual({ phase: 'done', progressPercent: 100 });
|
|
80
|
+
}, 30_000);
|
|
81
|
+
|
|
82
|
+
test('trimVideo can output webm', async () => {
|
|
83
|
+
const out = join(dir, 'trim.webm');
|
|
84
|
+
await trimVideo({
|
|
85
|
+
inputPath: source,
|
|
86
|
+
outputPath: out,
|
|
87
|
+
startSeconds: 1,
|
|
88
|
+
endSeconds: 3,
|
|
89
|
+
format: 'webm',
|
|
90
|
+
quality: 'low',
|
|
91
|
+
});
|
|
92
|
+
expect(await Bun.file(out).exists()).toBe(true);
|
|
93
|
+
}, 30_000);
|
|
94
|
+
|
|
95
|
+
test('extractThumbnail writes a single frame', async () => {
|
|
96
|
+
const out = join(dir, 'thumb.jpg');
|
|
97
|
+
await extractThumbnail(source, 4, out);
|
|
98
|
+
expect((await Bun.file(out).size) > 0).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('trimVideo rejects an invalid spec', async () => {
|
|
102
|
+
await expect(
|
|
103
|
+
trimVideo({
|
|
104
|
+
inputPath: source,
|
|
105
|
+
outputPath: join(dir, 'x.mp4'),
|
|
106
|
+
startSeconds: 5,
|
|
107
|
+
endSeconds: 5,
|
|
108
|
+
}),
|
|
109
|
+
).rejects.toThrow(/Invalid VideoTrimSpec/);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('probeMedia throws on a missing file', async () => {
|
|
113
|
+
await expect(probeMedia(join(dir, 'nope.mp4'))).rejects.toThrow();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// Video trim / probe / thumbnail via native ffmpeg + ffprobe, spawned with Bun.spawn.
|
|
2
|
+
// All argv construction + output parsing lives in the pure `ffmpeg.ts` (tested without
|
|
3
|
+
// ffmpeg); this file is the thin process shell. The binaries must be on PATH, or set
|
|
4
|
+
// `FFMPEG_PATH` / `FFPROBE_PATH`. Replaces ca's hand-rolled spawn boilerplate.
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
assertValidVideoTrimSpec,
|
|
8
|
+
type VideoProgressEvent,
|
|
9
|
+
type VideoTrimSpec,
|
|
10
|
+
} from 'cursedbelt-core/media';
|
|
11
|
+
import {
|
|
12
|
+
buildProbeArgs,
|
|
13
|
+
buildThumbnailArgs,
|
|
14
|
+
buildTrimArgs,
|
|
15
|
+
type ProbeResult,
|
|
16
|
+
parseFfmpegProgress,
|
|
17
|
+
parseFfprobe,
|
|
18
|
+
} from 'cursedbelt-core/media/ffmpeg';
|
|
19
|
+
import {
|
|
20
|
+
buildFrameJpegArgs,
|
|
21
|
+
buildStreamProbeArgs,
|
|
22
|
+
EMPTY_PROBE,
|
|
23
|
+
parseProgressMicros,
|
|
24
|
+
parseStreamProbe,
|
|
25
|
+
type VideoProbe,
|
|
26
|
+
} from './streaming';
|
|
27
|
+
|
|
28
|
+
const FFMPEG = process.env.FFMPEG_PATH || 'ffmpeg';
|
|
29
|
+
const FFPROBE = process.env.FFPROBE_PATH || 'ffprobe';
|
|
30
|
+
|
|
31
|
+
/** Is ffmpeg runnable? Cheap probe used to skip server media ops / tests cleanly. */
|
|
32
|
+
export async function isFfmpegAvailable(bin: string = FFMPEG): Promise<boolean> {
|
|
33
|
+
try {
|
|
34
|
+
const proc = Bun.spawn([bin, '-version'], { stdout: 'ignore', stderr: 'ignore' });
|
|
35
|
+
await proc.exited;
|
|
36
|
+
return proc.exitCode === 0;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Is ffprobe runnable? */
|
|
43
|
+
export function isFfprobeAvailable(): Promise<boolean> {
|
|
44
|
+
return isFfmpegAvailable(FFPROBE);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Drain a stderr stream line-by-line (ffmpeg uses \r to update progress in place). */
|
|
48
|
+
async function readProgress(
|
|
49
|
+
stream: ReadableStream<Uint8Array>,
|
|
50
|
+
totalSeconds: number,
|
|
51
|
+
onProgress: ((e: VideoProgressEvent) => void) | undefined,
|
|
52
|
+
): Promise<string> {
|
|
53
|
+
const decoder = new TextDecoder();
|
|
54
|
+
let buffer = '';
|
|
55
|
+
let tail = '';
|
|
56
|
+
for await (const chunk of stream) {
|
|
57
|
+
const text = decoder.decode(chunk, { stream: true });
|
|
58
|
+
buffer += text;
|
|
59
|
+
tail = (tail + text).slice(-4000); // keep a bounded tail for an error message
|
|
60
|
+
const segments = buffer.split(/[\r\n]/);
|
|
61
|
+
buffer = segments.pop() ?? '';
|
|
62
|
+
if (onProgress) {
|
|
63
|
+
for (const seg of segments) {
|
|
64
|
+
const ev = parseFfmpegProgress(seg, totalSeconds);
|
|
65
|
+
if (ev) onProgress(ev);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return tail;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Trim `[startSeconds, endSeconds]` from `inputPath` into `outputPath`, re-encoding to
|
|
74
|
+
* the requested format/quality. `onProgress` receives `encode` events (parsed from
|
|
75
|
+
* ffmpeg stderr) and a final `done`; on a non-zero exit it emits `error` and throws.
|
|
76
|
+
*/
|
|
77
|
+
export async function trimVideo(
|
|
78
|
+
spec: VideoTrimSpec,
|
|
79
|
+
onProgress?: (event: VideoProgressEvent) => void,
|
|
80
|
+
): Promise<void> {
|
|
81
|
+
assertValidVideoTrimSpec(spec);
|
|
82
|
+
const total = spec.endSeconds - spec.startSeconds;
|
|
83
|
+
onProgress?.({ phase: 'encode', progressPercent: 0 });
|
|
84
|
+
|
|
85
|
+
const proc = Bun.spawn([FFMPEG, ...buildTrimArgs(spec)], { stdout: 'ignore', stderr: 'pipe' });
|
|
86
|
+
const tail = await readProgress(proc.stderr as ReadableStream<Uint8Array>, total, onProgress);
|
|
87
|
+
await proc.exited;
|
|
88
|
+
|
|
89
|
+
if (proc.exitCode !== 0) {
|
|
90
|
+
const message = `ffmpeg trim failed (exit ${proc.exitCode}): ${
|
|
91
|
+
tail
|
|
92
|
+
.trim()
|
|
93
|
+
.split(/[\r\n]/)
|
|
94
|
+
.pop() ?? ''
|
|
95
|
+
}`;
|
|
96
|
+
onProgress?.({ phase: 'error', error: message });
|
|
97
|
+
throw new Error(message);
|
|
98
|
+
}
|
|
99
|
+
onProgress?.({ phase: 'done', progressPercent: 100 });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Probe a media file's primary video dimensions + duration + container format. */
|
|
103
|
+
export async function probeMedia(inputPath: string): Promise<ProbeResult> {
|
|
104
|
+
const proc = Bun.spawn([FFPROBE, ...buildProbeArgs(inputPath)], {
|
|
105
|
+
stdout: 'pipe',
|
|
106
|
+
stderr: 'pipe',
|
|
107
|
+
});
|
|
108
|
+
const stdout = await new Response(proc.stdout).text();
|
|
109
|
+
await proc.exited;
|
|
110
|
+
if (proc.exitCode !== 0) {
|
|
111
|
+
throw new Error(`ffprobe failed (exit ${proc.exitCode}) for ${inputPath}`);
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
return parseFfprobe(JSON.parse(stdout));
|
|
115
|
+
} catch (err) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`ffprobe returned unparseable JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Extract a single frame at `atSeconds` from `inputPath` and write it to `output`. */
|
|
123
|
+
export async function extractThumbnail(
|
|
124
|
+
inputPath: string,
|
|
125
|
+
atSeconds: number,
|
|
126
|
+
output: string,
|
|
127
|
+
): Promise<void> {
|
|
128
|
+
const proc = Bun.spawn([FFMPEG, ...buildThumbnailArgs(inputPath, atSeconds, output)], {
|
|
129
|
+
stdout: 'ignore',
|
|
130
|
+
stderr: 'pipe',
|
|
131
|
+
});
|
|
132
|
+
const tail = await new Response(proc.stderr).text();
|
|
133
|
+
await proc.exited;
|
|
134
|
+
if (proc.exitCode !== 0) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`ffmpeg thumbnail failed (exit ${proc.exitCode}): ${
|
|
137
|
+
tail
|
|
138
|
+
.trim()
|
|
139
|
+
.split(/[\r\n]/)
|
|
140
|
+
.pop() ?? ''
|
|
141
|
+
}`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Streaming-pipeline spawn runners (companions to streaming.ts's pure builders) ───────
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Codec-aware probe for the streaming pipeline (codecs + pixfmt + audio, so a caller can
|
|
150
|
+
* decide whether to stream-copy). Best-effort: returns {@link EMPTY_PROBE} on any failure —
|
|
151
|
+
* the pipeline stays usable (a node is always playable as raw range meanwhile).
|
|
152
|
+
*/
|
|
153
|
+
export async function probeVideoStream(inputPath: string): Promise<VideoProbe> {
|
|
154
|
+
try {
|
|
155
|
+
const proc = Bun.spawn([FFPROBE, ...buildStreamProbeArgs(inputPath)], {
|
|
156
|
+
stdout: 'pipe',
|
|
157
|
+
stderr: 'ignore',
|
|
158
|
+
});
|
|
159
|
+
const stdout = await new Response(proc.stdout).text();
|
|
160
|
+
await proc.exited;
|
|
161
|
+
if (proc.exitCode !== 0) return EMPTY_PROBE;
|
|
162
|
+
return parseStreamProbe(JSON.parse(stdout));
|
|
163
|
+
} catch {
|
|
164
|
+
return EMPTY_PROBE;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Run an ffmpeg transcode while parsing its `-progress pipe:1` stream into a 0..99 percent
|
|
170
|
+
* (so a slow re-encode reports live progress). `durationSec` is the source duration; 0 ⇒ no
|
|
171
|
+
* percent. `args` should NOT include the leading `ffmpeg`/`-y`. Resolves true on success;
|
|
172
|
+
* on failure it passes the trailing stderr to `onError` (the app logs) and resolves false.
|
|
173
|
+
*/
|
|
174
|
+
export async function runFfmpegProgress(
|
|
175
|
+
args: string[],
|
|
176
|
+
durationSec: number,
|
|
177
|
+
onProgress?: (pct: number) => void,
|
|
178
|
+
onError?: (stderrTail: string) => void,
|
|
179
|
+
): Promise<boolean> {
|
|
180
|
+
const proc = Bun.spawn([FFMPEG, '-y', '-progress', 'pipe:1', '-nostats', ...args], {
|
|
181
|
+
stdout: 'pipe',
|
|
182
|
+
stderr: 'pipe',
|
|
183
|
+
});
|
|
184
|
+
if (durationSec > 0 && onProgress && proc.stdout) {
|
|
185
|
+
void (async () => {
|
|
186
|
+
const decoder = new TextDecoder();
|
|
187
|
+
let buf = '';
|
|
188
|
+
try {
|
|
189
|
+
for await (const chunk of proc.stdout as ReadableStream<Uint8Array>) {
|
|
190
|
+
buf += decoder.decode(chunk, { stream: true });
|
|
191
|
+
const lines = buf.split('\n');
|
|
192
|
+
buf = lines.pop() ?? '';
|
|
193
|
+
for (const line of lines) {
|
|
194
|
+
const us = parseProgressMicros(line);
|
|
195
|
+
if (us === null) continue;
|
|
196
|
+
onProgress(Math.max(0, Math.min(99, Math.round((us / 1e6 / durationSec) * 100))));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
/* progress is best-effort */
|
|
201
|
+
}
|
|
202
|
+
})();
|
|
203
|
+
}
|
|
204
|
+
await proc.exited;
|
|
205
|
+
if (proc.exitCode !== 0) {
|
|
206
|
+
if (onError) onError((await new Response(proc.stderr).text()).slice(-600));
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* On-demand single JPEG frame at `atSeconds` (scaled to `maxWidth`) as bytes, or null on
|
|
214
|
+
* failure. `-ss` before `-i` fast-seeks — instant on a faststart file.
|
|
215
|
+
*/
|
|
216
|
+
export async function extractFrameJpeg(
|
|
217
|
+
filePath: string,
|
|
218
|
+
atSeconds: number,
|
|
219
|
+
maxWidth = 240,
|
|
220
|
+
): Promise<Uint8Array | null> {
|
|
221
|
+
try {
|
|
222
|
+
const proc = Bun.spawn([FFMPEG, '-y', ...buildFrameJpegArgs(filePath, atSeconds, maxWidth)], {
|
|
223
|
+
stdout: 'pipe',
|
|
224
|
+
stderr: 'ignore',
|
|
225
|
+
});
|
|
226
|
+
const bytes = new Uint8Array(await new Response(proc.stdout).arrayBuffer());
|
|
227
|
+
await proc.exited;
|
|
228
|
+
return proc.exitCode === 0 && bytes.length > 0 ? bytes : null;
|
|
229
|
+
} catch {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|