Adds Q/P values to output file metadata
This commit is contained in:
@@ -74,6 +74,7 @@
|
|||||||
- No explicit prioritization owner or milestone for the optimization backlog.
|
- No explicit prioritization owner or milestone for the optimization backlog.
|
||||||
- No benchmark or timing harness exists for startup, probe, DB, or conversion orchestration overhead.
|
- No benchmark or timing harness exists for startup, probe, DB, or conversion orchestration overhead.
|
||||||
- Repo hygiene is still mixed with generated artifacts and some clearly unfinished files.
|
- Repo hygiene is still mixed with generated artifacts and some clearly unfinished files.
|
||||||
|
- The legacy TMDB-backed `Scenario 4` path is currently blocked by a pattern/track regression: `Patterns must define at least one track before they can be stored.` This surfaced while rerunning TMDB-dependent checks after the zero-track pattern hardening.
|
||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
@@ -81,6 +82,7 @@
|
|||||||
2. Tackle the cheapest remaining product-surface cleanup first:
|
2. Tackle the cheapest remaining product-surface cleanup first:
|
||||||
- placeholder UI surfaces and dead helper cleanup.
|
- placeholder UI surfaces and dead helper cleanup.
|
||||||
3. Continue replacing oversized legacy test matrices with focused modern integration and unit coverage.
|
3. Continue replacing oversized legacy test matrices with focused modern integration and unit coverage.
|
||||||
|
4. Triage the legacy `Scenario 4` pattern/track failure and decide whether to fix the harness, adapt it to the zero-track guard, or retire that path during the ongoing test-suite migration.
|
||||||
|
|
||||||
## Delete When
|
## Delete When
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ that area.
|
|||||||
- Agents shall not silently substitute `python`, `python3`, or another interpreter for Python-side test work.
|
- Agents shall not silently substitute `python`, `python3`, or another interpreter for Python-side test work.
|
||||||
- If `~/.local/share/ffx.venv/bin/python` is missing or not executable, agents shall stop and report the missing venv instead of continuing with Python-side test execution.
|
- If `~/.local/share/ffx.venv/bin/python` is missing or not executable, agents shall stop and report the missing venv instead of continuing with Python-side test execution.
|
||||||
|
|
||||||
|
## Shell Environment Requirement
|
||||||
|
|
||||||
|
- Agents shall source `~/.bashrc` from an interactive Bash shell before running TMDB-dependent test commands or TMDB-dependent `python -m ffx ...` test invocations.
|
||||||
|
- Agents shall not source `~/.bashrc.d/interactive/77_tmdb.sh` directly for normal test work; `~/.bashrc` is the required entry point.
|
||||||
|
- In automation this means agents shall use an interactive Bash invocation such as `bash -ic 'source ~/.bashrc && ...'`, because a non-interactive `bash -lc` returns from `~/.bashrc` before the interactive fragments are loaded.
|
||||||
|
- If sourcing `~/.bashrc` still does not provide required shell environment such as `TMDB_API_KEY`, agents shall stop and report the missing environment instead of continuing with TMDB-dependent test execution.
|
||||||
|
|
||||||
## Current Harness
|
## Current Harness
|
||||||
|
|
||||||
- Entrypoint: `~/.local/share/ffx.venv/bin/python tests/legacy_runner.py run`
|
- Entrypoint: `~/.local/share/ffx.venv/bin/python tests/legacy_runner.py run`
|
||||||
|
|||||||
@@ -171,6 +171,18 @@ class FfxController():
|
|||||||
return [outputFilePath]
|
return [outputFilePath]
|
||||||
|
|
||||||
|
|
||||||
|
def generateEncodingMetadataTags(self, videoEncoder: VideoEncoder, quality, preset) -> dict:
|
||||||
|
metadataTags = {}
|
||||||
|
|
||||||
|
if videoEncoder in (VideoEncoder.AV1, VideoEncoder.H264, VideoEncoder.VP9):
|
||||||
|
metadataTags["ENCODING_QUALITY"] = str(quality)
|
||||||
|
|
||||||
|
if videoEncoder == VideoEncoder.AV1:
|
||||||
|
metadataTags["ENCODING_PRESET"] = str(preset)
|
||||||
|
|
||||||
|
return metadataTags
|
||||||
|
|
||||||
|
|
||||||
def generateAudioEncodingTokens(self):
|
def generateAudioEncodingTokens(self):
|
||||||
"""Generates ffmpeg options audio streams including channel remapping, codec and bitrate"""
|
"""Generates ffmpeg options audio streams including channel remapping, codec and bitrate"""
|
||||||
|
|
||||||
@@ -261,6 +273,11 @@ class FfxController():
|
|||||||
|
|
||||||
|
|
||||||
preset = presetFilters[0]['parameters']['preset'] if presetFilters else PresetFilter.DEFAULT_PRESET
|
preset = presetFilters[0]['parameters']['preset'] if presetFilters else PresetFilter.DEFAULT_PRESET
|
||||||
|
self.__context['encoding_metadata_tags'] = self.generateEncodingMetadataTags(
|
||||||
|
videoEncoder,
|
||||||
|
quality,
|
||||||
|
preset,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
filterParamTokens = []
|
filterParamTokens = []
|
||||||
|
|||||||
@@ -295,6 +295,9 @@ class MediaDescriptorChangeSet():
|
|||||||
+ f":{trackDescriptor.getSubIndex()}",
|
+ f":{trackDescriptor.getSubIndex()}",
|
||||||
f"{removeKey}="]
|
f"{removeKey}="]
|
||||||
|
|
||||||
|
for tagKey, tagValue in self.__context.get('encoding_metadata_tags', {}).items():
|
||||||
|
metadataTokens += [f"-metadata:g", f"{tagKey}={tagValue}"]
|
||||||
|
|
||||||
return metadataTokens
|
return metadataTokens
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
139
tests/unit/test_ffx_controller.py
Normal file
139
tests/unit/test_ffx_controller.py
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|
||||||
|
SRC_ROOT = Path(__file__).resolve().parents[2] / "src"
|
||||||
|
|
||||||
|
if str(SRC_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(SRC_ROOT))
|
||||||
|
|
||||||
|
|
||||||
|
from ffx.ffx_controller import FfxController # noqa: E402
|
||||||
|
from ffx.logging_utils import get_ffx_logger # noqa: E402
|
||||||
|
from ffx.media_descriptor import MediaDescriptor # noqa: E402
|
||||||
|
from ffx.track_codec import TrackCodec # noqa: E402
|
||||||
|
from ffx.track_descriptor import TrackDescriptor # noqa: E402
|
||||||
|
from ffx.track_type import TrackType # noqa: E402
|
||||||
|
from ffx.video_encoder import VideoEncoder # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
class StaticConfig:
|
||||||
|
def __init__(self, data: dict | None = None):
|
||||||
|
self._data = data or {}
|
||||||
|
|
||||||
|
def getData(self):
|
||||||
|
return self._data
|
||||||
|
|
||||||
|
|
||||||
|
class FfxControllerTests(unittest.TestCase):
|
||||||
|
def make_context(self, video_encoder: VideoEncoder) -> dict:
|
||||||
|
return {
|
||||||
|
"logger": get_ffx_logger(),
|
||||||
|
"config": StaticConfig(),
|
||||||
|
"video_encoder": video_encoder,
|
||||||
|
"dry_run": False,
|
||||||
|
"perform_cut": False,
|
||||||
|
"bitrates": {
|
||||||
|
"stereo": "112k",
|
||||||
|
"ac3": "256k",
|
||||||
|
"dts": "320k",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def make_media_descriptors(self) -> tuple[MediaDescriptor, MediaDescriptor]:
|
||||||
|
descriptor = MediaDescriptor(
|
||||||
|
track_descriptors=[
|
||||||
|
TrackDescriptor(
|
||||||
|
index=0,
|
||||||
|
source_index=0,
|
||||||
|
sub_index=0,
|
||||||
|
track_type=TrackType.VIDEO,
|
||||||
|
codec_name=TrackCodec.H264,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
source_descriptor = MediaDescriptor(
|
||||||
|
track_descriptors=[
|
||||||
|
TrackDescriptor(
|
||||||
|
index=0,
|
||||||
|
source_index=0,
|
||||||
|
sub_index=0,
|
||||||
|
track_type=TrackType.VIDEO,
|
||||||
|
codec_name=TrackCodec.H264,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return descriptor, source_descriptor
|
||||||
|
|
||||||
|
def test_vp9_run_job_emits_file_level_encoding_quality_metadata(self):
|
||||||
|
context = self.make_context(VideoEncoder.VP9)
|
||||||
|
target_descriptor, source_descriptor = self.make_media_descriptors()
|
||||||
|
controller = FfxController(context, target_descriptor, source_descriptor)
|
||||||
|
commands = []
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch.object(
|
||||||
|
controller,
|
||||||
|
"executeCommandSequence",
|
||||||
|
side_effect=lambda command: commands.append(command) or ("", "", 0),
|
||||||
|
),
|
||||||
|
patch("ffx.ffx_controller.os.path.exists", return_value=False),
|
||||||
|
):
|
||||||
|
controller.runJob(
|
||||||
|
"input.mkv",
|
||||||
|
"output.webm",
|
||||||
|
targetFormat="webm",
|
||||||
|
chainIteration=[
|
||||||
|
{
|
||||||
|
"identifier": "quality",
|
||||||
|
"parameters": {"quality": 27},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(2, len(commands))
|
||||||
|
self.assertIn("-metadata:g", commands[1])
|
||||||
|
self.assertIn("ENCODING_QUALITY=27", commands[1])
|
||||||
|
self.assertFalse(
|
||||||
|
any(token.startswith("ENCODING_PRESET=") for token in commands[1])
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_av1_run_job_emits_file_level_quality_and_preset_metadata(self):
|
||||||
|
context = self.make_context(VideoEncoder.AV1)
|
||||||
|
target_descriptor, source_descriptor = self.make_media_descriptors()
|
||||||
|
controller = FfxController(context, target_descriptor, source_descriptor)
|
||||||
|
commands = []
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
controller,
|
||||||
|
"executeCommandSequence",
|
||||||
|
side_effect=lambda command: commands.append(command) or ("", "", 0),
|
||||||
|
):
|
||||||
|
controller.runJob(
|
||||||
|
"input.mkv",
|
||||||
|
"output.webm",
|
||||||
|
targetFormat="webm",
|
||||||
|
chainIteration=[
|
||||||
|
{
|
||||||
|
"identifier": "quality",
|
||||||
|
"parameters": {"quality": 29},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "preset",
|
||||||
|
"parameters": {"preset": 7},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(1, len(commands))
|
||||||
|
self.assertIn("-metadata:g", commands[0])
|
||||||
|
self.assertIn("ENCODING_QUALITY=29", commands[0])
|
||||||
|
self.assertIn("ENCODING_PRESET=7", commands[0])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user