diff --git a/av/frame.py b/av/frame.py index 7e43de597..7c54ae2e2 100644 --- a/av/frame.py +++ b/av/frame.py @@ -1,7 +1,11 @@ import cython from cython.cimports.av.error import err_check from cython.cimports.av.opaque import opaque_container -from cython.cimports.av.utils import avrational_to_fraction, to_avrational +from cython.cimports.av.utils import ( + avdict_to_dict, + avrational_to_fraction, + to_avrational, +) from av.sidedata.sidedata import SideDataContainer @@ -178,6 +182,11 @@ def side_data(self): self._side_data = SideDataContainer(self) return self._side_data + @property + def metadata(self): + """Metadata attached to the frame by FFmpeg.""" + return avdict_to_dict(self.ptr.metadata, "utf-8", "strict") + def make_writable(self): """ Ensures that the frame data is writable. Copy the data to new buffer if it is not. diff --git a/av/frame.pyi b/av/frame.pyi index f085fc0f4..caed23482 100644 --- a/av/frame.pyi +++ b/av/frame.pyi @@ -14,6 +14,8 @@ class Frame: side_data: SideData opaque: object @property + def metadata(self) -> dict[str, str]: ... + @property def time(self) -> float | None: ... @property def is_corrupt(self) -> bool: ... diff --git a/scripts/build-deps b/scripts/build-deps index c2602e728..8ff3415c0 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -69,7 +69,7 @@ echo ./configure --disable-bsfs \ --enable-bsf=chomp,extract_extradata,h264_mp4toannexb,setts \ --disable-filters \ - --enable-filter=abuffer,abuffersink,aformat,aresample,atempo,buffer,buffersink,bwdif,color,loudnorm,lutrgb,overlay,palettegen,scale,testsrc,vflip,volume \ + --enable-filter=abuffer,abuffersink,aformat,aresample,atempo,buffer,buffersink,bwdif,color,ebur128,loudnorm,lutrgb,overlay,palettegen,scale,testsrc,vflip,volume \ --enable-sse \ --enable-avx \ --enable-avx2 \ diff --git a/tests/test_filters.py b/tests/test_filters.py index 97038828a..565df3507 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -193,6 +193,40 @@ def test_audio_buffer_volume_filter(self): assert np.allclose(input_data * 0.5, output_data) + def test_audio_frame_metadata(self) -> None: + graph = Graph() + graph.link_nodes( + graph.add_abuffer( + format="fltp", + sample_rate=48000, + layout="stereo", + time_base=Fraction(1, 48000), + ), + graph.add("ebur128", "metadata=1"), + graph.add("abuffersink"), + ).configure() + + frame = AudioFrame.from_ndarray( + np.zeros((2, 4800), dtype=np.float32), + format="fltp", + layout="stereo", + ) + frame.sample_rate = 48000 + frame.time_base = Fraction(1, 48000) + frame.pts = 0 + + graph.push(frame) + graph.push(None) + output = graph.pull() + metadata = output.metadata + + assert "lavfi.r128.I" in metadata + assert "lavfi.r128.LRA" in metadata + + # Frame.metadata returns a copy of the underlying AVDictionary. + metadata.clear() + assert "lavfi.r128.I" in output.metadata + def _test_video_buffer(self, graph): input_container = av.open(format="lavfi", file="color=c=pink:duration=1:r=30") input_video_stream = input_container.streams.video[0]