Skip to main content

media,static_site_tests: disallow VP9-encoded videos as well as AV1

ID
d8bb257
date
2026-08-26 21:18:10+00:00
author
Alex Chan <alex@alexwlchan.net>
parent
e5e2f88
message
media,static_site_tests: disallow VP9-encoded videos as well as AV1
changed files
15 files, 95 additions, 55 deletions

Changed files

CHANGELOG.md (7294 → 7690)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3323906..150ac9e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
 # CHANGELOG
 
+## v53 - 2026-08-26
+
+Static site tests now disallow videos encoded in VP9, in addition to the previously-disallowed AV1.
+
+The latest version of yt-dlp downloads YouTube videos in VP9, which I can't play on my Mac.
+
+To support this change, the `media.is_av1_video` function has been replaced with `media.get_video_codec`.
+This is exposed as a Python API and through a `get_video_codec` CLI tool.
+
 ## v52 - 2026-07-20
 
 Static site tests now allow keeping a Python `requirements.in` and `requirements.txt` file in the top-level of a static site folder.

javascript/chives-image.js (6561 → 6561)

diff --git a/javascript/chives-image.js b/javascript/chives-image.js
index ec63606..020efa3 100644
--- a/javascript/chives-image.js
+++ b/javascript/chives-image.js
@@ -1,5 +1,5 @@
 // javascript/chives-image.js
-// chives version: 52
+// chives version: 53
 
 // ChivesImage provides a <chives-image> component that renders
 // an ImageEntity on a web page.

javascript/chives-pagination.js (4929 → 4929)

diff --git a/javascript/chives-pagination.js b/javascript/chives-pagination.js
index 34b4f78..583b229 100644
--- a/javascript/chives-pagination.js
+++ b/javascript/chives-pagination.js
@@ -1,5 +1,5 @@
 // javascript/chives-pagination.js
-// chives version: 52
+// chives version: 53
 
 // ChivesPagination manages the pagination of a collection of items.
 //

javascript/chives-sorting.js (5034 → 5034)

diff --git a/javascript/chives-sorting.js b/javascript/chives-sorting.js
index 42c8208..343c6f5 100644
--- a/javascript/chives-sorting.js
+++ b/javascript/chives-sorting.js
@@ -1,5 +1,5 @@
 // javascript/chives-sorting.js
-// chives version: 52
+// chives version: 53
 
 // ChivesSorting manages the sorting of a collection of items.
 //

javascript/chives-video.js (5147 → 5147)

diff --git a/javascript/chives-video.js b/javascript/chives-video.js
index eaf2363..81f7773 100644
--- a/javascript/chives-video.js
+++ b/javascript/chives-video.js
@@ -1,5 +1,5 @@
 // javascript/chives-video.js
-// chives version: 52
+// chives version: 53
 
 // ChivesVideo provides a <chives-video> component that renders
 // a VideoEntity on a web page.

pyproject.toml (1958 → 2003)

diff --git a/pyproject.toml b/pyproject.toml
index 0d8b540..5b98774 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -42,6 +42,7 @@ urls = ["certifi"]
 chives-list-js = "chives.cli:list_chives_js"
 chives-update-js = "chives.cli:update_js_components_cli"
 chives-write-js = "chives.cli:write_chives_js"
+get_codec = "chives.cli:get_video_codec_cli"
 srt-to-vtt = "chives.cli:convert_srt_to_vtt_cli"
 
 [project.urls]

python/chives/__init__.py (391 → 391)

diff --git a/python/chives/__init__.py b/python/chives/__init__.py
index da3d17d..ade97f4 100644
--- a/python/chives/__init__.py
+++ b/python/chives/__init__.py
@@ -11,4 +11,4 @@ I share across multiple sites.
 
 """
 
-__version__ = "52"
+__version__ = "53"

python/chives/cli.py (2188 → 2452)

diff --git a/python/chives/cli.py b/python/chives/cli.py
index 4f730ca..c2cf322 100644
--- a/python/chives/cli.py
+++ b/python/chives/cli.py
@@ -8,7 +8,7 @@ from pathlib import Path
 import sys
 
 from chives.javascript import update_js_components
-from chives.media import convert_srt_to_vtt
+from chives.media import convert_srt_to_vtt, get_video_codec
 
 
 def convert_srt_to_vtt_cli() -> None:  # pragma: no cover
@@ -68,3 +68,15 @@ def write_chives_js() -> None:  # pragma: no cover
             print(filename)
         else:
             sys.exit(f"unrecognised JS file: {filename!r}")
+
+
+def get_video_codec_cli() -> None:  # pragma: no cover
+    """
+    Print the codec of a video file.
+    """
+    try:
+        path = sys.argv[1]
+    except IndexError:
+        sys.exit(f"Usage: {__file__} PATH")
+
+    print(get_video_codec(path))

python/chives/javascript.py (2388 → 2354)

diff --git a/python/chives/javascript.py b/python/chives/javascript.py
index a79c552..ae4218a 100644
--- a/python/chives/javascript.py
+++ b/python/chives/javascript.py
@@ -2,7 +2,6 @@
 Helper functions for working with the JavaScript components.
 """
 
-import glob
 from importlib.resources.abc import Traversable
 import os
 from pathlib import Path
@@ -46,7 +45,7 @@ def update_js_components(root: Path, js_assets: Traversable | Path) -> None:
     """
     print(f"Scanning for chives JavaScript components in: {root}")
 
-    target_files = glob.glob("**/chives-*.js", recursive=True, root_dir=root)
+    target_files = list(root.rglob("chives-*.js"))
 
     if not target_files:
         print("No 'chives-*.js' files found in the current directory.")
@@ -69,7 +68,7 @@ def update_js_components(root: Path, js_assets: Traversable | Path) -> None:
         except ValueError:
             pass
 
-        print(f"Updating {coloured(filepath, 'blue')}")
+        print(f"Updating {coloured(str(filepath), 'blue')}")
 
         Path(root / filepath).write_text(internal_file.read_text())
         updated_count += 1

python/chives/media.py (11912 → 11899)

diff --git a/python/chives/media.py b/python/chives/media.py
index 8a58113..d6a4283 100644
--- a/python/chives/media.py
+++ b/python/chives/media.py
@@ -30,16 +30,16 @@ __all__ = [
     "create_video_entity",
     "get_media_paths",
     "get_tint_colour",
-    "is_av1_video",
+    "get_video_codec",
     "ImageEntity",
     "MediaEntity",
     "VideoEntity",
 ]
 
 
-def is_av1_video(path: str | Path) -> bool:
+def get_video_codec(path: str | Path) -> str:
     """
-    Return True if a video is encoded with AV1, False otherwise.
+    Return a video's codec. For example h264, av1, vp9.
     """
     # fmt: off
     cmd = [
@@ -64,7 +64,7 @@ def is_av1_video(path: str | Path) -> bool:
 
     output = subprocess.check_output(cmd, text=True)
 
-    return output.strip() == "av1"
+    return output.strip()
 
 
 class ImageEntity(TypedDict):

python/chives/static_site_tests.py (8947 → 8973)

diff --git a/python/chives/static_site_tests.py b/python/chives/static_site_tests.py
index a921cce..dc553cc 100644
--- a/python/chives/static_site_tests.py
+++ b/python/chives/static_site_tests.py
@@ -7,7 +7,6 @@ import collections
 from collections.abc import Iterator
 import concurrent.futures
 from difflib import SequenceMatcher
-import glob
 import itertools
 import os
 from pathlib import Path
@@ -19,7 +18,7 @@ import pytest
 
 from chives import __version__, javascript
 from chives.dates import date_matches_any_format, find_all_dates
-from chives.media import is_av1_video
+from chives.media import get_video_codec
 from chives.urls import is_url_safe
 
 
@@ -180,28 +179,32 @@ class StaticSiteTestSuite[M](ABC):
         assert bad_paths == set(), f"Found paths which aren't URL safe: {bad_paths}"
 
     @pytest.mark.skipif("SKIP_AV1" in os.environ, reason="skip slow test")
-    def test_no_videos_are_av1(self, site_root: Path) -> None:
+    def test_videos_use_allowed_codecs(self, site_root: Path) -> None:
         """
-        No videos are encoded in AV1 (which doesn't play on my iPhone).
-
-        This test can be removed when I upgrade all my devices to ones with
-        hardware AV1 decoding support.
-
-        See https://alexwlchan.net/2025/av1-on-my-iphone/
+        Videos only use codecs that play on my devices.
         """
-        av1_videos = set()
+        bad_videos = set()
+        disallowed_codecs = {
+            "av1",  # AV1 doesn't play on my iPhone
+            "vp9",  # VP9 doesn't play on my Mac mini
+        }
+
+        affected_paths = [
+            p for p in site_root.rglob("*") if p.suffix in {".mp4", ".webm"}
+        ]
 
         with concurrent.futures.ThreadPoolExecutor() as executor:
             futures = {
-                executor.submit(is_av1_video, site_root / p): p
-                for p in glob.glob("**/*.mp4", root_dir=site_root, recursive=True)
+                executor.submit(get_video_codec, site_root / p): p
+                for p in affected_paths
             }
-
             concurrent.futures.wait(futures)
 
-            av1_videos = {p for fut, p in futures.items() if fut.result()}
+            bad_videos = {
+                p for fut, p in futures.items() if fut.result() in disallowed_codecs
+            }
 
-        assert av1_videos == set(), f"Found videos encoded with AV1: {av1_videos}"
+        assert bad_videos == set(), f"Found videos with disallowed codecs: {bad_videos}"
 
     def test_no_subtitles_are_srt(self, site_root: Path) -> None:
         """
@@ -210,7 +213,7 @@ class StaticSiteTestSuite[M](ABC):
 
         All subtitles should be encoded as VTT.
         """
-        srt_subtitles = set(glob.glob("**/*.srt", root_dir=site_root, recursive=True))
+        srt_subtitles = set(site_root.rglob("*.srt"))
 
         assert srt_subtitles == set(), (
             f"Found subtitles encoded as SRT: {srt_subtitles}"
@@ -268,7 +271,7 @@ class StaticSiteTestSuite[M](ABC):
         """
         bad_versions = {
             p
-            for p in glob.glob(f"{site_root}/**/chives-*.js", recursive=True)
+            for p in site_root.rglob("chives-*.js")
             if javascript.get_version(p) != __version__
         }
 

tests/fixtures/media/Big_Buck_Bunny_360_10s_1MB.webm (0 → 1052940)

diff --git a/tests/fixtures/media/Big_Buck_Bunny_360_10s_1MB.webm b/tests/fixtures/media/Big_Buck_Bunny_360_10s_1MB.webm
new file mode 100644
index 0000000..76f6275
Binary files /dev/null and b/tests/fixtures/media/Big_Buck_Bunny_360_10s_1MB.webm differ

tests/python/test_media.py (18739 → 19019)

diff --git a/tests/python/test_media.py b/tests/python/test_media.py
index 88b6e9c..ed9c9b0 100644
--- a/tests/python/test_media.py
+++ b/tests/python/test_media.py
@@ -11,7 +11,7 @@ from chives.media import (
     create_image_entity,
     create_video_entity,
     get_media_paths,
-    is_av1_video,
+    get_video_codec,
 )
 
 
@@ -23,13 +23,24 @@ def fixtures_dir() -> Path:
     return Path("tests/fixtures/media")
 
 
-def test_is_av1_video(fixtures_dir: Path) -> None:
-    """is_av1_video correctly detects AV1 videos."""
-    # These two videos were downloaded from
-    # https://test-videos.co.uk/sintel/mp4-h264 and
-    # https://test-videos.co.uk/sintel/mp4-av1
-    assert not is_av1_video(fixtures_dir / "Sintel_360_10s_1MB_H264.mp4")
-    assert is_av1_video(fixtures_dir / "Sintel_360_10s_1MB_AV1.mp4")
+@pytest.mark.parametrize(
+    "name, codec",
+    [
+        # These two videos were downloaded from
+        # https://test-videos.co.uk/sintel/mp4-h264 and
+        # https://test-videos.co.uk/sintel/mp4-av1
+        ("Sintel_360_10s_1MB_H264.mp4", "h264"),
+        ("Sintel_360_10s_1MB_AV1.mp4", "av1"),
+        # This video was downloaded from
+        # https://test-videos.co.uk/bigbuckbunny/webm-vp9
+        ("Big_Buck_Bunny_360_10s_1MB.webm", "vp9"),
+    ],
+)
+def test_get_video_codec(fixtures_dir: Path, name: str, codec: str) -> None:
+    """
+    get_video_codec correctly detects a video's codec.
+    """
+    assert get_video_codec(fixtures_dir / name) == codec
 
 
 class TestCreateImageEntity:

tests/python/test_static_site_tests.py (12144 → 12230)

diff --git a/tests/python/test_static_site_tests.py b/tests/python/test_static_site_tests.py
index 62597ac..42a6f8b 100644
--- a/tests/python/test_static_site_tests.py
+++ b/tests/python/test_static_site_tests.py
@@ -173,30 +173,35 @@ def test_checks_for_url_safe_paths(pytester: Pytester, site_root: Path) -> None:
     pytester.runpytest("-k", keyword).assert_outcomes(failed=1)
 
 
-def test_checks_for_av1_videos(pytester: Pytester, site_root: Path) -> None:
+@pytest.mark.parametrize(
+    "name, is_allowed_codec",
+    [
+        ("Sintel_360_10s_1MB_H264.mp4", True),
+        ("Sintel_360_10s_1MB_AV1.mp4", False),
+        ("Big_Buck_Bunny_360_10s_1MB.webm", False),
+    ],
+)
+def test_checks_video_encoding(
+    pytester: Pytester, site_root: Path, name: str, is_allowed_codec: bool
+) -> None:
     """
-    The tests check for AV1-encoded videos.
+    The tests check for videos with the wrong encoding.
     """
     create_pyfile(pytester, site_root)
 
-    keyword = "test_no_videos_are_av1"
+    keyword = "test_videos_use_allowed_codecs"
 
     # This should pass trivially when the site is empty.
     pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
 
-    # Copy in an H.264-encoded video, and check it's not flagged.
-    shutil.copyfile(
-        GIT_ROOT / "tests/fixtures/media/Sintel_360_10s_1MB_H264.mp4",
-        site_root / "Sintel_360_10s_1MB_H264.mp4",
-    )
-    pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
+    # Copy in an H.264-encoded video, and check it's flagged or not
+    # depending on whether the codec is allowed.
+    shutil.copyfile(GIT_ROOT / "tests/fixtures/media" / name, site_root / name)
 
-    # Copy in an AV1-encoded video, and check it's caught by the test
-    shutil.copyfile(
-        GIT_ROOT / "tests/fixtures/media/Sintel_360_10s_1MB_AV1.mp4",
-        site_root / "Sintel_360_10s_1MB_AV1.mp4",
-    )
-    pytester.runpytest("-k", keyword).assert_outcomes(failed=1)
+    if is_allowed_codec:
+        pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
+    else:
+        pytester.runpytest("-k", keyword).assert_outcomes(failed=1)
 
 
 def test_checks_for_srt_subtitles(pytester: Pytester, site_root: Path) -> None:

tests/test_versions.py (491 → 507)

diff --git a/tests/test_versions.py b/tests/test_versions.py
index bb19771..f78438e 100644
--- a/tests/test_versions.py
+++ b/tests/test_versions.py
@@ -2,7 +2,7 @@
 Check that the JavaScript files and Python versions are in sync.
 """
 
-import glob
+from pathlib import Path
 
 from chives import __version__
 from chives.javascript import get_version
@@ -12,7 +12,7 @@ def test_versions_in_sync() -> None:
     """
     The JavaScript components have the same version as the Python module.
     """
-    for filepath in glob.glob("javascript/*.js"):
+    for filepath in Path("javascript").glob("*.js"):
         print(filepath)
         assert get_version(filepath) == __version__, (
             f"want {__version__}, got {get_version(filepath)}"