@torrent-tv/proxy 2.87.5 → 2.87.7
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/.claude/commands/opsx/apply.md +155 -155
- package/.claude/commands/opsx/archive.md +160 -160
- package/.claude/commands/opsx/explore.md +174 -174
- package/.claude/commands/opsx/propose.md +109 -109
- package/.claude/commands/opsx/sync.md +143 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
- package/.claude/skills/openspec-explore/SKILL.md +289 -289
- package/.claude/skills/openspec-propose/SKILL.md +113 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
- package/.dockerignore +5 -5
- package/AGENTS.md +322 -322
- package/CHANGELOG.md +8 -0
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/biome.json +411 -411
- package/docs/browser-proxy-contract.md +101 -94
- package/docs/container-architecture.md +218 -218
- package/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +729 -729
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +267 -267
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +1 -1
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/audio-tracks/post.js +29 -0
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/files/get.js +100 -100
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +129 -129
- package/routes/api/transcode-sessions/post.js +160 -160
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +361 -361
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +84 -84
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/render-run-graph.js +173 -173
- package/server.js +4 -0
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +332 -332
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +776 -776
- package/services/encode/EncodeRuns.js +1229 -1229
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputOpening.js +755 -755
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2162 -2162
- package/services/encode/RunProgress.js +84 -84
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +416 -416
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +114 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +45 -45
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/hwaccel.js +1894 -1894
- package/services/encode/index.js +9 -9
- package/services/encode/learned-median.js +90 -90
- package/services/encode/output/EncodedOutput.js +57 -57
- package/services/encode/output/Output.js +131 -131
- package/services/encode/output/OutputCatalog.js +414 -414
- package/services/encode/output/OutputSpec.js +390 -390
- package/services/encode/output/Timeline.js +308 -308
- package/services/encode/output/cut-grid.js +107 -107
- package/services/encode/output/index.js +20 -20
- package/services/encode/output/ladder.js +32 -32
- package/services/encode/output/playlists.js +284 -284
- package/services/encode/output/rates.js +125 -125
- package/services/encode/output-key-format.js +21 -21
- package/services/encode/quality/EncodeCost.js +1083 -1083
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +28 -28
- package/services/encode/quality/QualityController.js +935 -935
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +254 -254
- package/services/encode/quality/output-format.js +266 -266
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +747 -747
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +437 -437
- package/services/encode/segment-formats/index.js +77 -77
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +311 -311
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/ContainerOrchestrator.js +149 -149
- package/services/media/KeyframeTables.js +316 -316
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleCues.js +520 -520
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/audio-inventory.js +245 -212
- package/services/media/container/AviContainer.js +266 -266
- package/services/media/container/Container.js +603 -603
- package/services/media/container/ContainerFactory.js +56 -56
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/MatroskaContainer.js +1494 -1494
- package/services/media/container/Mp4Container.js +1244 -1244
- package/services/media/container/SubtitleFileContainer.js +322 -322
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/playback-planner.js +131 -59
- package/services/media/tracks/AudioTrack.js +131 -131
- package/services/media/tracks/ContainerTrack.js +75 -75
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/TextSubtitleTrack.js +287 -287
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1146 -1146
- package/services/server/ViewerRequests.js +522 -522
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +40 -36
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +983 -983
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +516 -516
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1880 -1880
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1196 -1196
- package/services/storage/wire.js +175 -175
- package/services/torrent/Contents.js +324 -324
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +262 -262
- package/services/torrent/torrent-pool.js +2 -2
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/held-bytes.js +155 -155
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/pool-adapter.js +1 -1
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/transport/data-channel-handler.js +1356 -1356
- package/services/transport/delivery-probe.js +842 -842
- package/services/transport/health-collector.js +53 -53
- package/services/transport/nat-classifier.js +231 -231
- package/services/transport/packet-witness.js +808 -808
- package/services/transport/port-mapper.js +235 -235
- package/services/transport/registry-api.js +81 -81
- package/services/transport/tunnel-client.js +447 -447
- package/services/transport/usrsctp-state.js +175 -175
- package/services/transport/webrtc-manager.js +484 -484
- package/services/viewer/Assignments.js +340 -340
- package/services/viewer/PriorityMap.js +390 -390
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +657 -657
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-inventory.test.js +212 -176
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +707 -707
- package/test/available-share.test.js +76 -76
- package/test/background-fill.test.js +190 -190
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +115 -115
- package/test/concurrent-cost.test.js +148 -148
- package/test/container-media-info.test.js +228 -228
- package/test/contention.test.js +76 -76
- package/test/core-dumps.test.js +57 -57
- package/test/coverage-follows-the-disk.test.js +228 -228
- package/test/coverage-map.test.js +195 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
- package/test/cut-times-timeline.test.js +65 -65
- package/test/cuts-follow-published-grid.test.js +109 -109
- package/test/data-channel-frame.test.js +63 -63
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +35 -35
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +462 -462
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +283 -283
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +419 -419
- package/test/encode-plan-viewers.test.js +718 -718
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +161 -161
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +46 -46
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -29
- package/test/helpers/manager.js +57 -57
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframe-table-budget.test.js +102 -102
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +111 -111
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +292 -292
- package/test/matroska-cues-track.test.js +192 -192
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-output-one-session.test.js +204 -204
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +206 -206
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +88 -88
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +381 -381
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +560 -560
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +318 -318
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +162 -162
- package/test/quality-variants.test.js +1229 -1229
- package/test/rate-control-in-the-output.test.js +180 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +65 -65
- package/test/same-height-switch.test.js +664 -664
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-closed-on-both-branches.test.js +96 -96
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-judged-by-its-own-header.test.js +221 -221
- package/test/segment-serve-wiring.test.js +475 -475
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +323 -323
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-existing-output.test.js +102 -102
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/source-file.test.js +133 -133
- package/test/source-files-route.test.js +103 -103
- package/test/source-registry.test.js +18 -18
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +182 -182
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cue-walk.test.js +370 -370
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/subtitle-track-numbering.test.js +386 -386
- package/test/supply-margin.test.js +177 -177
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +407 -407
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +89 -89
- package/test/viewer-states.test.js +189 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- package/test/viewing-generations.test.js +390 -390
- package/test/visible-rung.test.js +37 -37
- package/test/wait-ledger.test.js +100 -100
- package/test/webrtc-shim.test.js +149 -149
- package/test/wedge-certainty.test.js +131 -131
- package/test/withdraw-piece-claim.test.js +212 -212
- package/test/worker-channel.test.js +158 -158
- package/utils/client-log-file.js +151 -151
- package/utils/logger.js +312 -312
- package/utils/parse-range.js +25 -25
- package/utils/perf.js +59 -59
- package/{services/torrent → utils}/torrent-source-key.js +38 -38
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { spawn } from "node:child_process";
|
|
10
10
|
import { logger } from "../../utils/logger.js";
|
|
11
11
|
import { Container } from "./container/Container.js";
|
|
12
|
-
import { buildAudioInventory } from "./audio-inventory.js";
|
|
12
|
+
import { buildAudioInventory, enrichAudioInventoryFromSidecar } from "./audio-inventory.js";
|
|
13
13
|
import {
|
|
14
14
|
parseFfmpegDurationSeconds,
|
|
15
15
|
parseFfmpegStartTimeSeconds,
|
|
@@ -35,7 +35,8 @@ const BODY_PREFETCH_BYTES = 16 * 1024 * 1024;
|
|
|
35
35
|
* Not a measurement, and nothing is derived from it: it is the point past which
|
|
36
36
|
* holding the viewer costs more than the language and flags being waited for —
|
|
37
37
|
* which the folder name supplies anyway, from the torrent's file list, at no
|
|
38
|
-
* cost. The
|
|
38
|
+
* cost. The read continues asynchronously after this response and is shared
|
|
39
|
+
* with the refresh request, so a later answer can use the same container read.
|
|
39
40
|
*/
|
|
40
41
|
const SIDECAR_HEADER_WAIT_MS = 3_000;
|
|
41
42
|
|
|
@@ -276,6 +277,7 @@ function buildDirectUrl(localBaseUrl, sourceKey, fileIndex) {
|
|
|
276
277
|
* @property {number} durationSeconds - Total media duration in seconds (0 if unknown).
|
|
277
278
|
* @property {number} videoWidth - Source coded width (0 if unknown).
|
|
278
279
|
* @property {number} videoHeight - Source coded height (0 if unknown).
|
|
280
|
+
* @property {boolean} audioTracksPending - At least one sidecar header timed out and can be refreshed later.
|
|
279
281
|
*/
|
|
280
282
|
|
|
281
283
|
/**
|
|
@@ -292,7 +294,7 @@ function buildDirectUrl(localBaseUrl, sourceKey, fileIndex) {
|
|
|
292
294
|
* a torrent file. Plans are cached per (sourceKey, fileIndex) pair.
|
|
293
295
|
*
|
|
294
296
|
* @param {PlaybackPlannerOptions} options
|
|
295
|
-
* @returns {{ getPlan: (params: { sourceKey: string, fileIndex: number, userAgent?: string }) => Promise<PlaybackPlan> }}
|
|
297
|
+
* @returns {{ getPlan: (params: { sourceKey: string, fileIndex: number, userAgent?: string }) => Promise<PlaybackPlan>, refreshAudioTracks: (params: { sourceKey: string, fileIndex: number }) => Promise<{ audioTracks: object[], pending: boolean }> }}
|
|
296
298
|
*/
|
|
297
299
|
export function createPlaybackPlanner({
|
|
298
300
|
ffmpegBin,
|
|
@@ -330,6 +332,8 @@ export function createPlaybackPlanner({
|
|
|
330
332
|
}) {
|
|
331
333
|
/** @type {Map<string, PlaybackPlan>} */
|
|
332
334
|
const cache = new Map();
|
|
335
|
+
const pendingSidecarHeaders = new Map();
|
|
336
|
+
const declaredAudioReads = new Map();
|
|
333
337
|
/**
|
|
334
338
|
* Full media info parsed from the SAME probe that produced the plan, cached
|
|
335
339
|
* under the same key so a transcode session can reuse it instead of running
|
|
@@ -415,8 +419,9 @@ export function createPlaybackPlanner({
|
|
|
415
419
|
* own tracks and the ones shipped as separate files beside it.
|
|
416
420
|
*
|
|
417
421
|
* Built here, in the plan, because the plan is what the viewer's menu is drawn
|
|
418
|
-
* from — so
|
|
419
|
-
*
|
|
422
|
+
* from — so every playable track gets a stable number before the file opens.
|
|
423
|
+
* Sidecar header fields may arrive later; they enrich an existing numbered
|
|
424
|
+
* entry without changing the inventory length. It is also what
|
|
420
425
|
* the master playlist's rendition group is built from, so the number in the
|
|
421
426
|
* menu and the number in the `a/<n>/` address are the same number by
|
|
422
427
|
* construction rather than by agreement.
|
|
@@ -425,67 +430,72 @@ export function createPlaybackPlanner({
|
|
|
425
430
|
* @param {object} torrent
|
|
426
431
|
* @param {number} fileIndex
|
|
427
432
|
* @param {object[]} bannerAudioTracks - The probe's own audio streams.
|
|
428
|
-
* @returns {Promise<import("./audio-inventory.js").AudioInventoryEntry[]>}
|
|
433
|
+
* @returns {Promise<{ audioTracks: import("./audio-inventory.js").AudioInventoryEntry[], pendingFileIndexes: number[] }>}
|
|
429
434
|
*/
|
|
430
|
-
async function
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
* given. The language the viewer actually sees is read from the FOLDER the
|
|
442
|
-
* release put it in, which is in the torrent's file list and needs no bytes
|
|
443
|
-
* at all.
|
|
444
|
-
*
|
|
445
|
-
* @param {number} wantedFileIndex
|
|
446
|
-
* @param {string} label
|
|
447
|
-
* @returns {Promise<object[]>}
|
|
448
|
-
*/
|
|
449
|
-
const declaredAudioOf = async (wantedFileIndex, label) => {
|
|
450
|
-
if (typeof declaredTracksOf !== "function") {
|
|
451
|
-
return [];
|
|
452
|
-
}
|
|
453
|
-
let timer = null;
|
|
454
|
-
try {
|
|
455
|
-
return await Promise.race([
|
|
456
|
-
declaredTracksOf({ sourceKey, fileIndex: wantedFileIndex }).then((tracks) => tracks
|
|
435
|
+
async function declaredAudioOf(sourceKey, wantedFileIndex, label) {
|
|
436
|
+
if (typeof declaredTracksOf !== "function") {
|
|
437
|
+
return { tracks: [], complete: false, timedOut: false };
|
|
438
|
+
}
|
|
439
|
+
const readKey = `${sourceKey}:${wantedFileIndex}`;
|
|
440
|
+
let readPromise = declaredAudioReads.get(readKey);
|
|
441
|
+
if (!readPromise) {
|
|
442
|
+
readPromise = Promise.resolve()
|
|
443
|
+
.then(() => declaredTracksOf({ sourceKey, fileIndex: wantedFileIndex }))
|
|
444
|
+
.then((tracks) => ({
|
|
445
|
+
tracks: tracks
|
|
457
446
|
.filter((track) => track?.type === "audio")
|
|
458
|
-
.sort((left, right) => (left.declaredIndex ?? 0) - (right.declaredIndex ?? 0))
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
})
|
|
463
|
-
|
|
464
|
-
if (tracks === null) {
|
|
465
|
-
logger.info(
|
|
466
|
-
`audio tracks: "${label}" did not answer within ` +
|
|
467
|
-
`${SIDECAR_HEADER_WAIT_MS / 1000}s — offered without what its header would say`
|
|
468
|
-
);
|
|
469
|
-
return [];
|
|
470
|
-
}
|
|
471
|
-
return Array.isArray(tracks) ? tracks : [];
|
|
447
|
+
.sort((left, right) => (left.declaredIndex ?? 0) - (right.declaredIndex ?? 0)),
|
|
448
|
+
complete: Array.isArray(tracks) && tracks.length > 0
|
|
449
|
+
}))
|
|
450
|
+
.catch((error) => {
|
|
451
|
+
logger.info(`audio tracks: "${label}" could not be read (${error?.message ?? error})`);
|
|
452
|
+
return { tracks: [], complete: false };
|
|
472
453
|
});
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
454
|
+
declaredAudioReads.set(readKey, readPromise);
|
|
455
|
+
readPromise.then(
|
|
456
|
+
() => { if (declaredAudioReads.get(readKey) === readPromise) declaredAudioReads.delete(readKey); },
|
|
457
|
+
() => { if (declaredAudioReads.get(readKey) === readPromise) declaredAudioReads.delete(readKey); }
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
let timer = null;
|
|
461
|
+
try {
|
|
462
|
+
return await Promise.race([
|
|
463
|
+
readPromise,
|
|
464
|
+
new Promise((resolve) => {
|
|
465
|
+
timer = setTimeout(() => resolve(null), SIDECAR_HEADER_WAIT_MS);
|
|
466
|
+
timer.unref?.();
|
|
467
|
+
})
|
|
468
|
+
]).then((tracks) => {
|
|
469
|
+
if (tracks === null) {
|
|
470
|
+
logger.info(
|
|
471
|
+
`audio tracks: "${label}" did not answer within ${SIDECAR_HEADER_WAIT_MS / 1000}s — ` +
|
|
472
|
+
"offered without what its header would say"
|
|
473
|
+
);
|
|
474
|
+
return { tracks: [], complete: false, timedOut: true };
|
|
479
475
|
}
|
|
480
|
-
|
|
481
|
-
|
|
476
|
+
return {
|
|
477
|
+
tracks: Array.isArray(tracks?.tracks) ? tracks.tracks : [],
|
|
478
|
+
complete: tracks?.complete === true,
|
|
479
|
+
timedOut: false
|
|
480
|
+
};
|
|
481
|
+
});
|
|
482
|
+
} catch (error) {
|
|
483
|
+
logger.info(`audio tracks: "${label}" could not be read (${error?.message ?? error})`);
|
|
484
|
+
return { tracks: [], complete: false, timedOut: false };
|
|
485
|
+
} finally {
|
|
486
|
+
if (timer !== null) clearTimeout(timer);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async function buildInventory(sourceKey, torrent, fileIndex, bannerAudioTracks) {
|
|
491
|
+
const banner = Array.isArray(bannerAudioTracks) ? bannerAudioTracks : [];
|
|
482
492
|
// The picture's own tracks: ffmpeg numbers them, the container declares what
|
|
483
493
|
// they are. Both readings, lined up and checked — see `audio-inventory.js`.
|
|
484
494
|
let embedded = banner.map((track) => ({ ...track, declaresDefault: false }));
|
|
485
495
|
if (banner.length > 0) {
|
|
486
496
|
// The picture's head is already downloaded — the codec probe just read it
|
|
487
497
|
// — so this is a parse and not a wait, but it is bounded like the rest.
|
|
488
|
-
const declared = await declaredAudioOf(fileIndex, "the picture");
|
|
498
|
+
const { tracks: declared } = await declaredAudioOf(sourceKey, fileIndex, "the picture");
|
|
489
499
|
const merged = Container.mergeAudioFlags(banner, declared);
|
|
490
500
|
embedded = merged.tracks;
|
|
491
501
|
logger.info(
|
|
@@ -505,7 +515,9 @@ export function createPlaybackPlanner({
|
|
|
505
515
|
file,
|
|
506
516
|
// A bare elementary stream — `.ac3`, `.dts`, `.mp3` — has no table to
|
|
507
517
|
// read, so nothing is asked of the swarm for it at all.
|
|
508
|
-
|
|
518
|
+
...(file.declaresTracks
|
|
519
|
+
? await declaredAudioOf(sourceKey, file.fileIndex, file.name)
|
|
520
|
+
: { tracks: [], timedOut: false })
|
|
509
521
|
}))
|
|
510
522
|
);
|
|
511
523
|
const inventory = buildAudioInventory({ embedded, videoFileIndex: fileIndex, sidecars });
|
|
@@ -521,7 +533,61 @@ export function createPlaybackPlanner({
|
|
|
521
533
|
.join(" ")
|
|
522
534
|
);
|
|
523
535
|
}
|
|
524
|
-
return
|
|
536
|
+
return {
|
|
537
|
+
audioTracks: inventory,
|
|
538
|
+
pendingFileIndexes: sidecars.filter((sidecar) => sidecar.timedOut).map((sidecar) => sidecar.file.fileIndex)
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async function refreshAudioTracks({ sourceKey, fileIndex }) {
|
|
543
|
+
const cacheKey = `${sourceKey}:${fileIndex}`;
|
|
544
|
+
const plan = cache.get(cacheKey);
|
|
545
|
+
if (!plan) {
|
|
546
|
+
return { audioTracks: Array.isArray(plan?.audioTracks) ? plan.audioTracks : [], pending: false };
|
|
547
|
+
}
|
|
548
|
+
const pendingIndexes = new Set(pendingSidecarHeaders.get(cacheKey) ?? []);
|
|
549
|
+
if (pendingIndexes.size === 0 || typeof declaredTracksOf !== "function") {
|
|
550
|
+
return { audioTracks: plan.audioTracks, pending: false };
|
|
551
|
+
}
|
|
552
|
+
const sourceRecord = sourceRegistry.get(sourceKey);
|
|
553
|
+
if (!sourceRecord) {
|
|
554
|
+
const error = new Error("Source key was not found.");
|
|
555
|
+
error.code = "SOURCE_NOT_FOUND";
|
|
556
|
+
throw error;
|
|
557
|
+
}
|
|
558
|
+
const torrent = await torrentPool.getTorrent(sourceRecord.sourceType, sourceRecord.source);
|
|
559
|
+
const sidecarFiles = sidecarsOf(torrent, fileIndex).audio;
|
|
560
|
+
for (const sidecar of sidecarFiles) {
|
|
561
|
+
if (!pendingIndexes.has(sidecar.fileIndex)) continue;
|
|
562
|
+
const read = await declaredAudioOf(sourceKey, sidecar.fileIndex, sidecar.name);
|
|
563
|
+
if (read.timedOut) continue;
|
|
564
|
+
if (read.tracks.length === 0) {
|
|
565
|
+
if (read.complete) pendingIndexes.delete(sidecar.fileIndex);
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
const audio = read.tracks
|
|
569
|
+
.filter((track) => track?.type === "audio")
|
|
570
|
+
.sort((left, right) => (left.declaredIndex ?? 0) - (right.declaredIndex ?? 0));
|
|
571
|
+
if (audio.length === 0) continue;
|
|
572
|
+
if (!enrichAudioInventoryFromSidecar(plan.audioTracks, sidecar, audio[0])) {
|
|
573
|
+
pendingIndexes.delete(sidecar.fileIndex);
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
// Keep the published inventory length and every address stable for this
|
|
577
|
+
// session. A late header enriches the offered first track; adding tracks
|
|
578
|
+
// would require replacing the HLS master playlist already loaded by the
|
|
579
|
+
// browser.
|
|
580
|
+
pendingIndexes.delete(sidecar.fileIndex);
|
|
581
|
+
if (audio.length > 1) {
|
|
582
|
+
logger.info(
|
|
583
|
+
`audio tracks: late header for "${sidecar.name}" describes ${audio.length} tracks; ` +
|
|
584
|
+
"the active inventory keeps its published track count"
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
pendingSidecarHeaders.set(cacheKey, [...pendingIndexes]);
|
|
589
|
+
plan.audioTracksPending = pendingIndexes.size > 0;
|
|
590
|
+
return { audioTracks: plan.audioTracks, pending: pendingIndexes.size > 0 };
|
|
525
591
|
}
|
|
526
592
|
|
|
527
593
|
function withHostTimings(plan) {
|
|
@@ -589,6 +655,8 @@ export function createPlaybackPlanner({
|
|
|
589
655
|
return Array.isArray(plan?.audioTracks) ? plan.audioTracks : [];
|
|
590
656
|
},
|
|
591
657
|
|
|
658
|
+
refreshAudioTracks,
|
|
659
|
+
|
|
592
660
|
getCachedMediaInfo({ sourceKey, fileIndex }) {
|
|
593
661
|
return mediaInfoCache.get(`${sourceKey}:${fileIndex}`) ?? null;
|
|
594
662
|
},
|
|
@@ -657,6 +725,7 @@ export function createPlaybackPlanner({
|
|
|
657
725
|
videoWidth: 0,
|
|
658
726
|
videoHeight: 0,
|
|
659
727
|
audioTracks: [],
|
|
728
|
+
audioTracksPending: false,
|
|
660
729
|
subtitleTracks: []
|
|
661
730
|
};
|
|
662
731
|
cache.set(cacheKey, plan);
|
|
@@ -750,6 +819,7 @@ export function createPlaybackPlanner({
|
|
|
750
819
|
);
|
|
751
820
|
}
|
|
752
821
|
const sidecars = sidecarsOf(torrent, fileIndex);
|
|
822
|
+
const inventory = await buildInventory(sourceKey, torrent, fileIndex, audioTracks ?? []);
|
|
753
823
|
const plan = {
|
|
754
824
|
mode: requiresTranscode ? "hls" : "direct",
|
|
755
825
|
directUrl,
|
|
@@ -765,7 +835,8 @@ export function createPlaybackPlanner({
|
|
|
765
835
|
// Full track inventory for the browser's audio/subtitle menus. The audio
|
|
766
836
|
// half spans the picture's own tracks AND the soundtracks shipped as
|
|
767
837
|
// files beside it, under one numbering — see `buildInventory`.
|
|
768
|
-
audioTracks:
|
|
838
|
+
audioTracks: inventory.audioTracks,
|
|
839
|
+
audioTracksPending: inventory.pendingFileIndexes.length > 0,
|
|
769
840
|
subtitleTracks: await withContainerDefaults(sourceKey, torrent, fileIndex, subtitleTracks ?? []),
|
|
770
841
|
// The files BESIDE this picture that belong to it, and what each one's
|
|
771
842
|
// own path says about the track in it. Both answers are made here, by
|
|
@@ -816,6 +887,7 @@ export function createPlaybackPlanner({
|
|
|
816
887
|
// prioritised by the prefetch above).
|
|
817
888
|
if (codecsDetected) {
|
|
818
889
|
cache.set(cacheKey, plan);
|
|
890
|
+
pendingSidecarHeaders.set(cacheKey, inventory.pendingFileIndexes);
|
|
819
891
|
// Cache the full media info from THIS probe's banner (same helpers the
|
|
820
892
|
// session manager uses) so createSession can skip its own probe.
|
|
821
893
|
mediaInfoCache.set(cacheKey, {
|
|
@@ -1,131 +1,131 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Audio track — extends ContainerTrack with audio-specific flags.
|
|
3
|
-
*
|
|
4
|
-
* Matroska RFC 9559 §5.1.4.1:
|
|
5
|
-
* - FlagOriginal 0x55AE (default 0) — track is original language.
|
|
6
|
-
* - FlagCommentary 0x55AF — commentary track.
|
|
7
|
-
* - FlagVisualImpaired 0x55AC — audio description for visually impaired.
|
|
8
|
-
* - FlagTextDescriptions 0x55AD, FlagHearingImpaired etc. may also appear
|
|
9
|
-
* but the three above drive the audio menu (research/container-spec-conformance).
|
|
10
|
-
* MP4 ISO/IEC 14496-12: tkhd alternate_group groups alternate audio tracks.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { ContainerTrack } from "./ContainerTrack.js";
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Codec identifiers as containers write them, against the name ffmpeg prints.
|
|
17
|
-
*
|
|
18
|
-
* Needed because the browser decides whether it can play a soundtrack from that
|
|
19
|
-
* name, and for a sidecar file there is no ffmpeg banner to read it from — the
|
|
20
|
-
* track came from the container's own table, where Matroska writes `A_AC3` and
|
|
21
|
-
* MP4 writes `ac-3` for the thing ffmpeg calls `ac3`. Only what a soundtrack can
|
|
22
|
-
* actually be is listed; an identifier not here is reported as it was written,
|
|
23
|
-
* which the browser treats as one it does not know and therefore transcodes.
|
|
24
|
-
*/
|
|
25
|
-
const CODEC_NAMES = new Map([
|
|
26
|
-
["A_AAC", "aac"],
|
|
27
|
-
["A_AC3", "ac3"],
|
|
28
|
-
["A_EAC3", "eac3"],
|
|
29
|
-
["A_DTS", "dts"],
|
|
30
|
-
["A_FLAC", "flac"],
|
|
31
|
-
["A_OPUS", "opus"],
|
|
32
|
-
["A_VORBIS", "vorbis"],
|
|
33
|
-
["A_TRUEHD", "truehd"],
|
|
34
|
-
["A_MPEG/L3", "mp3"],
|
|
35
|
-
["A_MPEG/L2", "mp2"],
|
|
36
|
-
["A_ALAC", "alac"],
|
|
37
|
-
["mp4a", "aac"],
|
|
38
|
-
["ac-3", "ac3"],
|
|
39
|
-
["ec-3", "eac3"],
|
|
40
|
-
["alac", "alac"],
|
|
41
|
-
["opus", "opus"],
|
|
42
|
-
["Opus", "opus"],
|
|
43
|
-
["fLaC", "flac"],
|
|
44
|
-
["flac", "flac"]
|
|
45
|
-
]);
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Extensions of raw elementary streams, against the codec they carry.
|
|
49
|
-
*
|
|
50
|
-
* A bare `.ac3` has no track table to read, and its extension is the only thing
|
|
51
|
-
* that states its codec — which for an elementary stream is exactly what the
|
|
52
|
-
* extension means.
|
|
53
|
-
*/
|
|
54
|
-
const CODEC_BY_EXTENSION = new Map([
|
|
55
|
-
[".aac", "aac"],
|
|
56
|
-
[".ac3", "ac3"],
|
|
57
|
-
[".eac3", "eac3"],
|
|
58
|
-
[".dts", "dts"],
|
|
59
|
-
[".dtshd", "dts"],
|
|
60
|
-
[".flac", "flac"],
|
|
61
|
-
[".mp3", "mp3"],
|
|
62
|
-
[".mp2", "mp2"],
|
|
63
|
-
[".opus", "opus"],
|
|
64
|
-
[".ogg", "vorbis"],
|
|
65
|
-
[".oga", "vorbis"],
|
|
66
|
-
[".wav", "pcm"],
|
|
67
|
-
[".thd", "truehd"],
|
|
68
|
-
[".mlp", "truehd"],
|
|
69
|
-
[".m4a", "aac"]
|
|
70
|
-
]);
|
|
71
|
-
|
|
72
|
-
export class AudioTrack extends ContainerTrack {
|
|
73
|
-
/**
|
|
74
|
-
* @param {object} params - See ContainerTrack plus:
|
|
75
|
-
* @param {boolean} params.isOriginal - FlagOriginal
|
|
76
|
-
* @param {boolean} params.isCommentary - FlagCommentary
|
|
77
|
-
* @param {boolean} params.isVisualImpaired - FlagVisualImpaired / descriptive audio
|
|
78
|
-
* @param {number | null} params.channels
|
|
79
|
-
* @param {number | null} params.samplingFrequency
|
|
80
|
-
*/
|
|
81
|
-
constructor(params) {
|
|
82
|
-
super({ ...params, type: "audio" });
|
|
83
|
-
this.isOriginal = params.isOriginal === true;
|
|
84
|
-
this.isCommentary = params.isCommentary === true;
|
|
85
|
-
this.isVisualImpaired = params.isVisualImpaired === true;
|
|
86
|
-
this.channels = Number.isFinite(params.channels) ? params.channels : null;
|
|
87
|
-
this.samplingFrequency = Number.isFinite(params.samplingFrequency) ? params.samplingFrequency : null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* The ffmpeg-side codec name for one track, from whatever the reading gave.
|
|
92
|
-
*
|
|
93
|
-
* @param {{ codec?: string, codecId?: string }} track
|
|
94
|
-
* @param {string} [extension] - The sidecar file's extension, when the track
|
|
95
|
-
* came from a file with no readable table.
|
|
96
|
-
* @returns {string}
|
|
97
|
-
*/
|
|
98
|
-
static codecNameOf(track, extension = "") {
|
|
99
|
-
const fromBanner = typeof track?.codec === "string" ? track.codec.trim() : "";
|
|
100
|
-
if (fromBanner.length > 0) {
|
|
101
|
-
return fromBanner.toLowerCase();
|
|
102
|
-
}
|
|
103
|
-
const codecId = typeof track?.codecId === "string" ? track.codecId.trim() : "";
|
|
104
|
-
if (codecId.length > 0) {
|
|
105
|
-
const known = CODEC_NAMES.get(codecId);
|
|
106
|
-
if (known) {
|
|
107
|
-
return known;
|
|
108
|
-
}
|
|
109
|
-
// Matroska allows a suffix — `A_AAC/MPEG4/LC`, `A_PCM/INT/LIT` — so the
|
|
110
|
-
// family is what the first two segments say.
|
|
111
|
-
const family = codecId.split("/").slice(0, 2).join("/");
|
|
112
|
-
const byFamily = CODEC_NAMES.get(family) ?? CODEC_NAMES.get(codecId.split("/")[0]);
|
|
113
|
-
if (byFamily) {
|
|
114
|
-
return byFamily;
|
|
115
|
-
}
|
|
116
|
-
if (codecId.startsWith("A_PCM")) {
|
|
117
|
-
return "pcm";
|
|
118
|
-
}
|
|
119
|
-
return codecId.toLowerCase();
|
|
120
|
-
}
|
|
121
|
-
return CODEC_BY_EXTENSION.get(extension) ?? "";
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** Human label helper — commentary and descriptive audio must not look like main track. */
|
|
125
|
-
audioRoleLabel() {
|
|
126
|
-
if (this.isCommentary) return "commentary";
|
|
127
|
-
if (this.isVisualImpaired) return "descriptive";
|
|
128
|
-
if (this.isOriginal) return "original";
|
|
129
|
-
return "";
|
|
130
|
-
}
|
|
131
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file Audio track — extends ContainerTrack with audio-specific flags.
|
|
3
|
+
*
|
|
4
|
+
* Matroska RFC 9559 §5.1.4.1:
|
|
5
|
+
* - FlagOriginal 0x55AE (default 0) — track is original language.
|
|
6
|
+
* - FlagCommentary 0x55AF — commentary track.
|
|
7
|
+
* - FlagVisualImpaired 0x55AC — audio description for visually impaired.
|
|
8
|
+
* - FlagTextDescriptions 0x55AD, FlagHearingImpaired etc. may also appear
|
|
9
|
+
* but the three above drive the audio menu (research/container-spec-conformance).
|
|
10
|
+
* MP4 ISO/IEC 14496-12: tkhd alternate_group groups alternate audio tracks.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { ContainerTrack } from "./ContainerTrack.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Codec identifiers as containers write them, against the name ffmpeg prints.
|
|
17
|
+
*
|
|
18
|
+
* Needed because the browser decides whether it can play a soundtrack from that
|
|
19
|
+
* name, and for a sidecar file there is no ffmpeg banner to read it from — the
|
|
20
|
+
* track came from the container's own table, where Matroska writes `A_AC3` and
|
|
21
|
+
* MP4 writes `ac-3` for the thing ffmpeg calls `ac3`. Only what a soundtrack can
|
|
22
|
+
* actually be is listed; an identifier not here is reported as it was written,
|
|
23
|
+
* which the browser treats as one it does not know and therefore transcodes.
|
|
24
|
+
*/
|
|
25
|
+
const CODEC_NAMES = new Map([
|
|
26
|
+
["A_AAC", "aac"],
|
|
27
|
+
["A_AC3", "ac3"],
|
|
28
|
+
["A_EAC3", "eac3"],
|
|
29
|
+
["A_DTS", "dts"],
|
|
30
|
+
["A_FLAC", "flac"],
|
|
31
|
+
["A_OPUS", "opus"],
|
|
32
|
+
["A_VORBIS", "vorbis"],
|
|
33
|
+
["A_TRUEHD", "truehd"],
|
|
34
|
+
["A_MPEG/L3", "mp3"],
|
|
35
|
+
["A_MPEG/L2", "mp2"],
|
|
36
|
+
["A_ALAC", "alac"],
|
|
37
|
+
["mp4a", "aac"],
|
|
38
|
+
["ac-3", "ac3"],
|
|
39
|
+
["ec-3", "eac3"],
|
|
40
|
+
["alac", "alac"],
|
|
41
|
+
["opus", "opus"],
|
|
42
|
+
["Opus", "opus"],
|
|
43
|
+
["fLaC", "flac"],
|
|
44
|
+
["flac", "flac"]
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extensions of raw elementary streams, against the codec they carry.
|
|
49
|
+
*
|
|
50
|
+
* A bare `.ac3` has no track table to read, and its extension is the only thing
|
|
51
|
+
* that states its codec — which for an elementary stream is exactly what the
|
|
52
|
+
* extension means.
|
|
53
|
+
*/
|
|
54
|
+
const CODEC_BY_EXTENSION = new Map([
|
|
55
|
+
[".aac", "aac"],
|
|
56
|
+
[".ac3", "ac3"],
|
|
57
|
+
[".eac3", "eac3"],
|
|
58
|
+
[".dts", "dts"],
|
|
59
|
+
[".dtshd", "dts"],
|
|
60
|
+
[".flac", "flac"],
|
|
61
|
+
[".mp3", "mp3"],
|
|
62
|
+
[".mp2", "mp2"],
|
|
63
|
+
[".opus", "opus"],
|
|
64
|
+
[".ogg", "vorbis"],
|
|
65
|
+
[".oga", "vorbis"],
|
|
66
|
+
[".wav", "pcm"],
|
|
67
|
+
[".thd", "truehd"],
|
|
68
|
+
[".mlp", "truehd"],
|
|
69
|
+
[".m4a", "aac"]
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
export class AudioTrack extends ContainerTrack {
|
|
73
|
+
/**
|
|
74
|
+
* @param {object} params - See ContainerTrack plus:
|
|
75
|
+
* @param {boolean} params.isOriginal - FlagOriginal
|
|
76
|
+
* @param {boolean} params.isCommentary - FlagCommentary
|
|
77
|
+
* @param {boolean} params.isVisualImpaired - FlagVisualImpaired / descriptive audio
|
|
78
|
+
* @param {number | null} params.channels
|
|
79
|
+
* @param {number | null} params.samplingFrequency
|
|
80
|
+
*/
|
|
81
|
+
constructor(params) {
|
|
82
|
+
super({ ...params, type: "audio" });
|
|
83
|
+
this.isOriginal = params.isOriginal === true;
|
|
84
|
+
this.isCommentary = params.isCommentary === true;
|
|
85
|
+
this.isVisualImpaired = params.isVisualImpaired === true;
|
|
86
|
+
this.channels = Number.isFinite(params.channels) ? params.channels : null;
|
|
87
|
+
this.samplingFrequency = Number.isFinite(params.samplingFrequency) ? params.samplingFrequency : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The ffmpeg-side codec name for one track, from whatever the reading gave.
|
|
92
|
+
*
|
|
93
|
+
* @param {{ codec?: string, codecId?: string }} track
|
|
94
|
+
* @param {string} [extension] - The sidecar file's extension, when the track
|
|
95
|
+
* came from a file with no readable table.
|
|
96
|
+
* @returns {string}
|
|
97
|
+
*/
|
|
98
|
+
static codecNameOf(track, extension = "") {
|
|
99
|
+
const fromBanner = typeof track?.codec === "string" ? track.codec.trim() : "";
|
|
100
|
+
if (fromBanner.length > 0) {
|
|
101
|
+
return fromBanner.toLowerCase();
|
|
102
|
+
}
|
|
103
|
+
const codecId = typeof track?.codecId === "string" ? track.codecId.trim() : "";
|
|
104
|
+
if (codecId.length > 0) {
|
|
105
|
+
const known = CODEC_NAMES.get(codecId);
|
|
106
|
+
if (known) {
|
|
107
|
+
return known;
|
|
108
|
+
}
|
|
109
|
+
// Matroska allows a suffix — `A_AAC/MPEG4/LC`, `A_PCM/INT/LIT` — so the
|
|
110
|
+
// family is what the first two segments say.
|
|
111
|
+
const family = codecId.split("/").slice(0, 2).join("/");
|
|
112
|
+
const byFamily = CODEC_NAMES.get(family) ?? CODEC_NAMES.get(codecId.split("/")[0]);
|
|
113
|
+
if (byFamily) {
|
|
114
|
+
return byFamily;
|
|
115
|
+
}
|
|
116
|
+
if (codecId.startsWith("A_PCM")) {
|
|
117
|
+
return "pcm";
|
|
118
|
+
}
|
|
119
|
+
return codecId.toLowerCase();
|
|
120
|
+
}
|
|
121
|
+
return CODEC_BY_EXTENSION.get(extension) ?? "";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Human label helper — commentary and descriptive audio must not look like main track. */
|
|
125
|
+
audioRoleLabel() {
|
|
126
|
+
if (this.isCommentary) return "commentary";
|
|
127
|
+
if (this.isVisualImpaired) return "descriptive";
|
|
128
|
+
if (this.isOriginal) return "original";
|
|
129
|
+
return "";
|
|
130
|
+
}
|
|
131
|
+
}
|