# Changelog

All notable changes to the FFmpeg Media module will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed
- **FFmpeg filtergraph escaping:** `SubtitlesProcessor.escapeFilterPath()` now
  escapes `]`, `,`, and `;` in addition to `\`, `:`, `'`, `[` — prevents
  filter chain injection from subtitle file paths.
- **Two-pass null device portability:** First-pass output uses
  `PHP_OS_FAMILY`-based null device selection (`NUL` on Windows, `/dev/null`
  on Unix).
- **Two-pass log directory validation:** `AdvancedSettingsForm` validates
  stream URI scheme and absolute path; `TwoPassEncodingService` throws
  `RuntimeException` if the directory cannot be resolved after creation
  instead of silently concatenating `false`.
- **Dashboard listing memory:** Replaced full-array materialization with
  streaming DB cursor processed in batches of 50. Peak memory reduced from
  O(n) to O(batch_size) while maintaining exact access-filtered pager totals.
- **TranscodeForm force re-transcode:** `force` checkbox now queues with
  `operation = 'reconvert'`, matching Drush `--force` semantics (existing
  outputs deleted by QueueWorker before encoding).
- **TranscodeForm clear_existing race:** Queue check moved before output
  deletion to prevent clearing outputs when another request already queued
  the same media.
- **TranscodeForm output validation:** Empty output selection now triggers a
  form validation error instead of silently falling back to all profile
  outputs.
- **Queue backlog consistency:** Health endpoint, dashboard, and Drush
  `ffmpeg:system-status` now use `getQueuedMediaCount()` from the dedup
  marker table instead of `numberOfItems()` from the Drupal queue backend,
  which can include orphaned items.
- **Probe denial log flooding:** Lowered severity from `warning` to `notice`
  and added per-process burst limit (5 entries) to prevent log flooding from
  repeated invalid health probe token requests.

### Deprecated
- **CDN config credentials:** Reading `access_key` and `secret_key` from
  exportable configuration is deprecated since alpha7 and will be removed in
  `ffmpeg_media:2.0.0`. Use environment variables (`FFMPEG_MEDIA_S3_ACCESS_KEY`,
  `FFMPEG_MEDIA_S3_SECRET_KEY`), the Key module, or the admin settings form
  (State API). See `docs/SECURITY.md` for the migration guide.

## [1.0.0-alpha7] - 2026-03-20

### Added
- **Video filter composition layer:** `VideoFilterCollectorInterface` collects
  filters from tagged `VideoFilterProviderInterface` services and injects them
  into `FfmpegCommandBuilder` during conversion. Supports all output formats
  (MP4, WebM, MKV, HLS, DASH), hardware + software pipelines, and adaptive
  streaming. Filters are composed with weight-based ordering and chained via
  `-filter_complex`.
- `WatermarkFilterProvider` (ffmpeg_media_advanced): burns watermark into every
  converted output during transcoding instead of creating a separate
  post-processed file. Supports image overlay and text watermarks.
- Access-controlled thumbnail/preview serving via private file system with
  `hook_file_download()` access gate and `hook_runtime_requirements()` warning
  when `private://` is not configured.
- `OutputExecutionRunner` service: extracted output iteration loop, single-output
  conversion, and GPU two-pass routing from `VideoConverter`
- `MediaConversionInspector` service: extracted status/eligibility checks and
  converted file deletion from `VideoConverter`
- `MediaArtifactManager` service: complete artifact cleanup (CDN, streaming dirs,
  progress/queue state) used by `MediaConversionInspector`
- `ConversionDependencies` value object: bundles 10 pipeline services for
  `VideoConverter` constructor injection
- Deterministic Playwright bootstrap via `scripts/prepare-e2e-user.sh`, including
  dedicated E2E admin provisioning and runtime metadata for probe-token-aware
  API assertions
- Real functional integration coverage for `videojs` and `plyr` formatter
  output replacement via `ThirdPartyFormatterIntegrationTest`

### Changed
- **VideoConverter god-class decomposition:** constructor reduced from 20
  parameters to 5 via `ConversionDependencies` value object (bundles 10 pipeline
  services). Output execution loop extracted to `OutputExecutionRunner` (owns
  GPU two-pass routing, adaptive streaming delegation, per-output conversion).
  Inspection methods (`canReconvert`, `getStatus`, `deleteConvertedFiles`,
  `isEligibleForConversion`) extracted to `MediaConversionInspector` (backed by
  `MediaArtifactManager` for complete artifact cleanup including CDN).
- `OutputExecutionException` now carries `HardwareType` context for circuit
  breaker tracking on storage failures
- Playwright E2E preparation now rebuilds Drupal caches by default to keep the
  live dev container in sync with current service definitions and routes
- Clarified the BC boundary: public services are not automatically supported
  contrib extension API; most remaining public services are runtime-public
  internals
- `BitrateAnalyzerInterface` remains runtime-public after validation of the
  current adaptive streaming plugin instantiation path

### Fixed
- **Queue editor access-safe pagination:** Queue list responses now paginate
  after visibility filtering and global queue sorting. Hidden media items no
  longer inflate `meta.total` / `has_more`, and reordered queues no longer
  produce empty phantom pages.
- **Dashboard listing reuse:** The UI dashboard now caches exact access-filtered
  totals for a short period, reducing repeated full scans on identical
  status-filter requests.
- **Standalone QA runner:** Added `scripts/qa-runner.php` so `composer run qa:*`
  works both inside a Drupal checkout and from a standalone repo clone when
  `DRUPAL_PROJECT_ROOT` is set.
- **Subtitle font sanitization:** `SubtitlesProcessor` now sanitizes `font_name`
  before embedding it in the ASS `force_style` string.
- **Command helper field expansion:** Internal Drush helper methods now expand
  media IDs only across fields configured for the entity's actual bundle.
- **Legacy output-directory normalization:** Added update hooks for core and
  image-module output storage settings. Existing absolute filesystem paths are
  normalized to supported Drupal stream-wrapper URIs on update.
- **Output storage hardening:** Core and image settings forms now reject
  absolute output directories, runtime requirements warn on invalid persisted
  output paths, and `GifProcessor` fails fast on unsupported filesystem paths
  instead of silently storing unusable outputs.
- QA documentation now reflects the currently revalidated PHP 8.5.2 harness
  state and no longer overstates full-suite reruns that were not remeasured in
  the current patch session.
- **Multi-field correctness:** Field context (`fieldName`) now propagated
  end-to-end through adaptive streaming conversion. `AdaptiveStreamingConverter`
  uses explicit field context instead of re-resolving the preferred context,
  preventing cross-field collisions on multi-field media entities.
- **Field-scoped artifact paths:** Single-file outputs and HLS/DASH streaming
  directories now include a `/{fieldName}/` segment between media ID and format
  when a field name is provided. Prevents physical file collisions when
  multiple fields on the same media entity target the same output format.
  Legacy single-field layout preserved via empty-string default.
- **Drush `ffmpeg:process` safety parity:** Queue processing commands now
  respect global pause, acquire/release conversion locks, enforce retry backoff,
  store permanent failures in dead-letter storage, and track progress on
  failure — matching the `VideoConversionQueueWorker` safety semantics.
- **VideoPlayerFormatter progress scoping:** Progress reads now scoped to the
  formatter's field via `getProgress($mediaId, $fieldName)` instead of
  unscoped `getProgress($mediaId)`.
- **UI field awareness:** `TranscodeForm`, `AjaxController.progress`, and
  `SseProgressController` accept optional `?field_name=` query parameter
  for explicit field scoping. All `MediaOperationsService` calls in
  `TranscodeForm` pass the resolved field name.
- `SegmentCleanupService` scans both legacy (`{mediaId}/{format}/`) and
  field-scoped (`{mediaId}/{fieldName}/{format}/`) directory layouts.
- Restored public visibility for `CdnManagerInterface`, required by
  `CdnSettingsForm::create()`
- Removed unused `MenuHooks` from the public runtime service surface
- **CDN field-scoped cleanup:** `MediaArtifactManager.deleteRemoteArtifacts()`
  no longer triggers a media-wide CDN prefix sweep during field-scoped
  deletions, preventing cross-field CDN artifact destruction.
- **Status page field consistency:** `DashboardController.status()` now
  passes `$fieldName` to `buildVideoPreview()`, `buildTranscriptSection()`,
  `getVideoInfo()`, and `buildActionButtons()` for correct field-scoped
  rendering.
- **Status page cache context:** Added `url.query_args:field_name` to status
  page render cache so field-specific content is not served from stale cache.
- **Dashboard access cacheability:** Added `user` cache context to the main
  dashboard for correct per-entity access filtering.
- **Whisper transcribe link field scope:** `buildActionButtons()` now passes
  `$fieldRouteOptions` to the Whisper transcribe link, consistent with
  transcode and poster links.
- **TranscribeForm field context:** Reads `?field_name=` query parameter,
  uses `getContextForField()` for field-specific context resolution, and
  preserves field name in the redirect URL.
- **Formatter config cache tags:** `ConvertedVideoFormatter` now includes
  `config:ffmpeg_media.settings` in cache tags; `VideoPlayerFormatter`
  includes both `config:ffmpeg_media.settings` and
  `config:ffmpeg_media_player.settings`.

## [1.0.0-alpha6] - 2026-03-08

### Added
- **Royalty-free codec defaults:** All audio codec defaults changed from `aac`
  to `libopus`. All processor video codec defaults changed from `libx264` to
  `libsvtav1`. New `AudioCodecResolver` service negotiates between the user's
  global preference and each container's supported codecs with intelligent
  royalty-free fallback (libopus > flac > mp3 > libvorbis). Invalid config
  values validated against allowlist. Existing installs keep their configured
  codec; new installs default to libopus.
- GPU AV1/Opus WebM+MP4 fallback profile and HLS/DASH streaming profiles
- Redesigned profiles page with grouped row layout
- Vendored fully local Vidstack player runtime (no external CDN)
- GitLab CI pipeline with PHPStan, PHPCS, and PHPUnit gates
- CDN S3 integration test with MinIO
- Standalone E2E support with `DRUPAL_PROJECT_ROOT`
- Cached resolved video sources to reduce DB queries

### Changed
- Reduced public service surface and added third-party formatter schema
- Replaced outdated field mapping references with formatter-based terminology

### Fixed
- **7 validated bugs:** State machine transitions, stuck conversion detection,
  error message disclosure, segment cleanup on reconvert, queue processing
  parity with QueueWorker safety semantics.
- **SSE queue stream:** Access filtering and segment cleanup in reconvert path.
- **Rate limiter:** Sliding-window interpolation, field context propagation,
  cache tags, schema type corrections, stale profile reference cleanup.
- Subtitle language propagation in player output.
- Vidstack player controls and code review improvements.
- CI test failures: skip missing deps, fix form validation, restore public
  services for Controller `#[Autowire]`.

## [1.0.0-alpha5] - 2026-03-06

### Added
- `VideoSourceResolver` public service (`VideoSourceResolverInterface`) for
  third-party formatter integration; returns `ResolvedSources` value object
  (sources, poster, subtitles, isConverted)
- `MediaViewAlterHook` injects converted sources into any third-party video
  formatter (Video.js, etc.) via `hook_media_view_alter()`
- `OutputType` enum for type-safe output type references
- `ConfigValueProvider` service for centralized config value access
- `ConversionStatus` enum replacing string status constants
- Queue pause/resume functionality in queue editor
- Profile cloning (duplicate existing profiles)
- AJAX validation in ProfileForm for real-time feedback
- Null duration handling for streams without duration metadata
- `ffmpeg:dead-letters` Drush command (alias `ffdl`) for inspecting failed queue items
- Player keyboard shortcut help dialog with accessible overlay
- `TwoPassEncodingServiceInterface` moved to core namespace (decouples from
  `ffmpeg_media_advanced` submodule)
- CDN functional, kernel, and unit test coverage expansion
- Image processor tests reorganized into `ffmpeg_media_image` submodule
- Per-request caching in `FormatterSettingsResolver` with `resetCache()` method
- `manage ffmpeg queue` permission for queue editor operations
- Cross-field validation: `large_file_threshold` vs `max_file_size`;
  SSE heartbeat vs max stream duration
- Configurable `active_count_cache_ttl` (default 5.0s) for ProgressTracker
- Format priority validation in `ConvertedVideoFormatter` settings form
- WCAG 2.5.5 touch targets (44px min-height) for queue editor inputs
- Vidstack player fully vendored locally with HLS.js and DASH.js (no external
  runtime requests in local mode, GDPR-compliant); Google Cast disabled in local
  mode

### Changed
- `ConvertedVideoFormatter` and `VideoPlayerFormatter` delegate source
  resolution to `VideoSourceResolver` (shared logic, no duplication)
- Poster image rendered as server-side `<img>` overlay, removed on `playing`
  event (replaces Vidstack `<media-poster>` which lacks visibility management)
- Player default library source changed from `cdn` to `local` (fully vendored,
  no external runtime dependencies)
- Player requirements hook now validates shipped `js/vendor/vidstack` assets
  instead of local `node_modules`
- Queue editor routes use `manage ffmpeg queue` permission instead of
  `manage media outputs`
- Internal development docs removed from repository

### Fixed
- Poster not staying visible until playback: Vidstack `<media-poster>` replaced
  with CSS overlay `<img>` removed on `playing` event
- GPU hardware encoding pipeline: CUDA format mismatch with software encoders,
  `scale_cuda`/`scale_vaapi` rewrite for HW pipeline
- ETA formatting overflow in progress display
- BitrateAnalyzer caching producing stale results
- Reduced-motion accessibility for pulse/spin animations
- BEM prefix namespace consistency across CSS classes
- Batch checkbox touch targets below 44px minimum
- `ConversionContext` now `final readonly class` (was mutable)
- Profile set-default confirmation dialog
- Prometheus metric label validation (invalid characters rejected)

## [1.0.0-alpha4] - 2026-03-02

### Added
- Formatter-based conversion configuration via Manage Display (replaces
  FieldMapping config entity); `FormatterSettingsResolver`, `SourceFileResolver`,
  `ConversionContext` value object
- Table-based output storage (`ffmpeg_media_outputs` DB table) managed by
  `OutputStorageService` with `file.usage` tracking (replaces entity field storage)
- `PluginFactory` service wrapping Codec, Output, and Processor plugin managers
- `ConversionProfile` and `ProfileOutput` value objects for typed profile handling
- Audio codec compatibility validation in output plugins
- ProfileForm validates audio codec ↔ container and two-pass ↔ codec compatibility
- Default profile selection dropdown and set-as-default action
- Dynamic formatter settings with AJAX profile selection
- CDN improvements and update hooks for schema migrations
- 10 predefined royalty-free conversion profiles as install configs
- Patent compliance UI (codec licensing notice in profile form)

### Removed
- `FieldMapping` config entity, `FieldMapper` service, and `FieldMappingForm`
  (replaced by formatter-based configuration)
- Field-based output storage (replaced by `ffmpeg_media_outputs` DB table)
- `EncodeSettings` value object (merged into `ConversionOptions`)
- Redundant "Streaming Configuration" section from TranscodeForm
- Legacy dead methods, stale comments, and unused service aliases

### Changed
- `ConversionOptions` default audio codec changed from `libopus` to `aac`
- FLV output `supportedCodecs` split: video codecs only; audio codecs in
  `supportedAudioCodecs` (AAC, MP3)
- Default conversion profile ID changed from `'default'` to `'web_optimized'`
- GPU hardware encoding pipeline fixed for multi-output collision

### Fixed
- `av1_high_quality` install config: MP4 output used `libopus` (incompatible),
  changed to `aac`
- VP9 codec missing `flv` in `supportedContainers`
- Removed dead `vp8` from WHIP and RTP output `supportedCodecs`
- Player `crossorigin` attribute default changed to `none` (fixes same-origin
  playback without CORS headers)
- 3 phantom config keys removed from player submodule
- 14 bugs found and fixed during QA audit

## [1.0.0-alpha3] - 2026-02-26

### Added
- Update hooks for FieldMapping → formatter-based migration
- CDN hardening (TLS verification, XXE-safe XML parsing)
- Two-pass encoding fix for multi-output profiles
- WCAG/ARIA improvements for dashboard and admin pages
- SSE idle timeout configuration
- Temp-file cron cleanup for orphaned conversion artifacts
- `LibjxlCodec` plugin for JPEG XL image output

### Changed
- `StreamingVideoFormatter` renamed to `VideoPlayerFormatter`
- Dashboard output list: exclude transcript section, show KB for small files

### Fixed
- HLS/DASH CUDA format mismatch in adaptive streaming filter_complex
- JXL CUDA format mismatch and `image2` muxer errors
- JXL border color in dashboard output list

## [1.0.0-alpha2] - 2026-02-24

### Added
- Audio codec compatibility table in README.md
- `ffmpeg:transcribe` Drush command for CLI-based Whisper transcription
- DB-backed progress throttle (`last_write` column in `ffmpeg_media_progress`)
- Dashboard skip navigation links (8 section targets)
- Codec licensing notice section in README.md

### Changed
- Audio fields hidden via `#states` when JXL container is selected

## [1.0.0-alpha1] - 2026-02-23

First public alpha release.

### Core Module

- Video transcoding with FFmpeg 8.0+ support
- Plugin-based architecture for codecs (7), outputs (9), and processors (13)
- Hardware acceleration detection (NVENC, CUDA, VAAPI, QSV, VideoToolbox,
  AMF, Vulkan, OpenHarmony) with circuit breaker pattern
- Queue-based processing with dual-queue system (standard/large files)
- Real-time progress tracking via AJAX and SSE with configurable throttling
- Automatic retry with exponential backoff
- Lock manager for concurrent conversion limiting
- Comprehensive admin settings form with 10 config tabs
- Health check API endpoints (status, liveness, readiness) with token-based
  authentication (secure-by-default) and audit logging on denied access
- Prometheus-compatible metrics endpoint
- 15 Drush commands for CLI management
- Full CRUD system for conversion profiles (list, add, edit, delete)
- Per-title bitrate ladder analyzer
- Hardware pipeline builder (hwupload/init/device filters)
- CDN integration (S3-compatible) with upload, URL rewriting, presigned URLs,
  CloudFront invalidation, TLS-hardened cURL, and XXE-safe XML parsing
- `hook_file_url_alter()` for transparent CDN URL rewriting of output files
- Streaming directory upload (HLS/DASH manifests + segments)
- Persistent CDN path mapping via KeyValueStore
- 10 granular permissions for access control
- 4 event types (started, completed, failed, progress)
- Themed admin pages with Gin-compatible design system (CSS custom properties,
  BEM, dark mode)
- Path traversal validation in command builder (stream wrapper allowlist,
  `../` rejection, null byte check)
- Orphaned HLS/DASH segment cleanup on conversion failure and via cron
- Dead-letter cleanup via cron (90-day retention)
- Row-locked progress updates to prevent lost writes from concurrent workers
- 24 accepted input video formats (mp4, webm, mkv, avi, mov, flv, wmv, ts,
  mpg, mpeg, 3gp, 3g2, vob, asf, rm, rmvb, f4v, divx, dv, m2v, and more)

### Codec Plugins

- H264Codec (libx264, hardware encoders)
- HEVCCodec (libx265, hardware encoders)
- VP9Codec (libvpx-vp9)
- AV1Codec (libsvtav1, hardware encoders)
- VVCCodec (libvvenc) — experimental
- ProResCodec (prores_ks)
- FFV1Codec (ffv1) — lossless archival

### Output Plugins

- Mp4Output (MP4 container)
- WebmOutput (WebM container)
- MkvOutput (Matroska container)
- FlvOutput (Flash Video)
- JxlOutput (JPEG XL images)
- HlsOutput (HLS adaptive streaming)
- DashOutput (DASH adaptive streaming)
- WhipOutput (WHIP low-latency)
- RtpOutput (RTP real-time transport)

### Processor Plugins

- VideoInfoProcessor — extract video metadata as JSON
- ManifestProcessor — generate manifest files (JSON/YAML)
- AudioNormalizeProcessor — audio normalization (EBU R128, peak, RMS)
- MultiAudioProcessor — multi-audio track processing
- ThumbnailProcessor — video thumbnail extraction
- GifProcessor — animated GIF generation with palette optimization
- SegmentThumbnailProcessor — segment thumbnail generation
- SegmentGifProcessor — segment GIF generation
- HdrProcessor — HDR tone mapping
- WatermarkProcessor — overlay and text watermarks
- SubtitlesProcessor — subtitle extraction and conversion
- NormalizeProcessor — audio normalization
- WhisperProcessor — AI transcription (VTT subtitles)

### Value Objects

- ConversionOptions, ConversionResult, ConversionContext, ConversionProfile,
  ProfileOutput, ProgressData, VideoInfo
- AudioProfile, GopSettings, RateControlSettings, HlsOptions, DashOptions
- HwAccelOptions, BitrateRung, AudioTrack, SubtitleTrack, ErrorCategory
- HardwareType and CircuitBreakerState backed enums

### Submodules

#### ffmpeg_media_streaming
- HLS adaptive streaming output with portrait-aware representation ladder
- DASH adaptive streaming output
- WHIP output for low-latency streaming
- RTP output for real-time transport

#### ffmpeg_media_image
- ThumbnailProcessor, GifProcessor, SegmentThumbnailProcessor,
  SegmentGifProcessor
- ThumbnailGeneratorService

#### ffmpeg_media_advanced
- HDR processor, watermark processor, subtitles processor
- Audio normalization (EBU R128, peak, RMS)
- Two-pass encoding service

#### ffmpeg_media_ui
- Admin dashboard with queue stats, active conversions, recent failures
- Poster selector and conversion actions
- Real-time progress UI (SSE + polling fallback)
- Batch operations for bulk media management
- Queue editor with drag-and-drop reordering
- Accessible dialogs with focus trapping and 44 px touch targets

#### ffmpeg_media_player
- Vidstack-based video player with custom UI (settings gear, PiP, mini
  player, fullscreen)
- HLS/DASH adaptive streaming with progressive fallback
- Format priority selection (MP4, WebM, MKV, FLV)
- Per-field player configuration (no `drupalSettings` collisions)

#### ffmpeg_media_whisper
- Whisper processor for AI transcription (VTT subtitles)

### Technical Details

- PHP 8.5+ with `declare(strict_types=1)` in every file
- Drupal 11.3+/12 compatibility (`^11.3 || ^12`)
- OOP Hooks using `#[Hook]` attribute (`skip_procedural_hook_scan`)
- PHPUnit 11 inventory: 3,953 tests (`3,548` unit, `28` integration,
  `207` kernel, `170` functional)
- Current PHP QA reruns on PHP 8.5.2: full `composer run qa:ci` gate green
  (PHPStan 0 errors, PHPCS 0 errors / 0 warnings, unit/integration/kernel/
  functional suites green)
- 72 E2E tests (Playwright)

---

[Unreleased]: https://www.drupal.org/project/ffmpeg_media
[1.0.0-alpha5]: https://www.drupal.org/project/ffmpeg_media/releases/1.0.0-alpha5
[1.0.0-alpha4]: https://www.drupal.org/project/ffmpeg_media/releases/1.0.0-alpha4
[1.0.0-alpha3]: https://www.drupal.org/project/ffmpeg_media/releases/1.0.0-alpha3
[1.0.0-alpha2]: https://www.drupal.org/project/ffmpeg_media/releases/1.0.0-alpha2
[1.0.0-alpha1]: https://www.drupal.org/project/ffmpeg_media/releases/1.0.0-alpha1
