diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml new file mode 100644 index 00000000..01c9bcc5 --- /dev/null +++ b/.github/workflows/unit.yml @@ -0,0 +1,31 @@ +name: unit + +on: [ push, pull_request ] + +jobs: + test: + strategy: + fail-fast: false + matrix: + ubuntu-version: [ "ubuntu-22.04", "ubuntu-24.04", "ubuntu-latest" ] + python-version: [ "3.10", "3.11", "3.12" ] + runs-on: ${{ matrix.ubuntu-version }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + lfs: 'true' + - name: Setup python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt setuptools + sudo sysctl kernel.dmesg_restrict=0 + - name: Test "list" + run: ./vaapi-fits list + - name: Test "self" + run: ./vaapi-fits run --pl NA --device /dev/null -vv --parallel-metrics --call-timeout 10 --ctapr 0 test/self diff --git a/lib/codecs.py b/lib/codecs.py index 2191734e..aff50976 100644 --- a/lib/codecs.py +++ b/lib/codecs.py @@ -19,6 +19,6 @@ class Codec(str, enum.Enum): MJPEG = "mjpeg" MPEG2 = "mpeg2" VC1 = "vc1" - + VVC = "vvc" def __str__(self): return self.value diff --git a/lib/ffmpeg/decoderbase.py b/lib/ffmpeg/decoderbase.py index 3db243a0..15b99e0b 100644 --- a/lib/ffmpeg/decoderbase.py +++ b/lib/ffmpeg/decoderbase.py @@ -27,6 +27,7 @@ class Decoder(PropertyHandler, BaseFormatMapper): decoded = property(lambda s: s._decoded) osdecoded = property(lambda s: filepath2os(s.decoded)) hwaccel = property(lambda s: s.props["hwaccel"]) + strict = property(lambda s: s.ifprop("strict", "-strict {strict}")) width = property(lambda s: s.props["width"]) height = property(lambda s: s.props["height"]) @@ -82,7 +83,7 @@ def decode(self): self._statsfile = get_media().artifacts.reserve(mtype) return call( - f"{exe2os('ffmpeg')} -v verbose {self.hwinit}" + f"{exe2os('ffmpeg')} -v verbose {self.strict} {self.hwinit}" f" {self.ffdecoder} -r:v {fps} -i {self.ossource}" f" -f rawvideo -pix_fmt {self.format} -s:v {self.width}x{self.height}" f" -r:v {fps} {self.refseek} -i {self.osreference}" @@ -91,7 +92,7 @@ def decode(self): ) return call( - f"{exe2os('ffmpeg')} -v verbose {self.hwinit}" + f"{exe2os('ffmpeg')} -v verbose {self.strict} {self.hwinit}" f" {self.ffdecoder} -i {self.ossource} -lavfi '{self.scale_range}'" f" -c:v rawvideo -pix_fmt {self.format} -fps_mode passthrough" f" -noautoscale -vframes {self.frames} -y {self.ffoutput}" diff --git a/lib/ffmpeg/encoderbase.py b/lib/ffmpeg/encoderbase.py index 9c58ecd8..7fdf23dd 100755 --- a/lib/ffmpeg/encoderbase.py +++ b/lib/ffmpeg/encoderbase.py @@ -274,7 +274,7 @@ def check_bitrate(self): # acceptable bitrate within 10% of bitrate assert(bitrate_gap <= 0.10) - elif "vbr" == self.rcmode and vars(self).get("maxframesize", None) is None: + elif "vbr" in self.rcmode and vars(self).get("maxframesize", None) is None: # acceptable bitrate within 25% of minrate and 10% of maxrate if vars(self).get("maxrate", None) is not None: assert(self.minrate * 0.75 <= bitrate_actual <= self.maxrate * 1.10) diff --git a/lib/gstreamer/encoderbase.py b/lib/gstreamer/encoderbase.py index 4355a934..01245424 100755 --- a/lib/gstreamer/encoderbase.py +++ b/lib/gstreamer/encoderbase.py @@ -204,7 +204,7 @@ def check_bitrate(self): # acceptable bitrate within 10% of bitrate assert(bitrate_gap <= 0.10) - elif self.rcmode in ["vbr", "la_vbr"] and vars(self).get("maxframesize", None) is None: + elif "vbr" in self.rcmode and vars(self).get("maxframesize", None) is None: # acceptable bitrate within 25% of minrate and 10% of maxrate assert(self.minrate * 0.75 <= bitrate_actual <= self.maxrate * 1.10) diff --git a/lib/gstreamer/msdk/encoder.py b/lib/gstreamer/msdk/encoder.py index c701b53d..7d16510f 100644 --- a/lib/gstreamer/msdk/encoder.py +++ b/lib/gstreamer/msdk/encoder.py @@ -9,7 +9,7 @@ import slash from ....lib.gstreamer.encoderbase import BaseEncoderTest, Encoder as GstEncoder -from ....lib.gstreamer.util import have_gst_element +from ....lib.gstreamer.util import have_gst_element, get_elements from ....lib.gstreamer.msdk.util import using_compatible_driver, mapprofile, map_best_hw_format, mapformat from ....lib.gstreamer.msdk.decoder import Decoder from ....lib import platform @@ -142,6 +142,22 @@ def before(self): super().before() os.environ["GST_MSDK_DRM_DEVICE"] = get_media().render_device + self.__rank_before = os.environ.get("GST_PLUGIN_FEATURE_RANK", None) + # WA: Fix the gst-discoverer-1.0.exe report Missing plugins problem on Windows OS + if platform.info()['os'] == 'wsl': + ranks = [] if self.__rank_before is None else self.__rank_before.split(',') + ranks += [f"{e}:MAX" for e in get_elements("msdk")] + os.environ["GST_PLUGIN_FEATURE_RANK"] = ','.join(ranks) + + def after(self): + super().after() + + if None == self.__rank_before: + if 'GST_PLUGIN_FEATURE_RANK' in os.environ: + del os.environ["GST_PLUGIN_FEATURE_RANK"] + else: + os.environ["GST_PLUGIN_FEATURE_RANK"] = self.__rank_before + def map_profile(self): return mapprofile(self.codec, self.profile) diff --git a/lib/gstreamer/msdk/transcoder.py b/lib/gstreamer/msdk/transcoder.py index a5f07d29..89e3f287 100644 --- a/lib/gstreamer/msdk/transcoder.py +++ b/lib/gstreamer/msdk/transcoder.py @@ -54,10 +54,10 @@ class TranscoderTest(BaseTranscoderTest): ), ), Codec.VP9 : dict( - hw = (platform.get_caps("decode", "vp9_8"), have_gst_element(f"msdkvp9dec"), f"matroskademux ! vp9parse ! msdkvp9dec"), + hw = (platform.get_caps("decode", "vp9_8"), have_gst_element(f"msdkvp9dec"), "vp9parse ! msdkvp9dec"), ), Codec.AV1 : dict( - hw = (platform.get_caps("decode", "av1_8"), have_gst_element(f"msdkav1dec"), f"matroskademux ! av1parse ! msdkav1dec"), + hw = (platform.get_caps("decode", "av1_8"), have_gst_element(f"msdkav1dec"), "av1parse ! msdkav1dec"), ), }, encode = { diff --git a/lib/gstreamer/transcoderbase.py b/lib/gstreamer/transcoderbase.py index 174b73f7..c2d98dc6 100755 --- a/lib/gstreamer/transcoderbase.py +++ b/lib/gstreamer/transcoderbase.py @@ -117,12 +117,21 @@ def validate_caps(self): self.post_validate() + def get_demuxer(self, fname): + ext = os.path.splitext(fname)[1] + return { + ".ivf" : " ! ivfparse", + ".webm" : " ! matroskademux", + ".ts" : " ! tsdemux" + }.get(ext, "") + def gen_input_opts(self): self.ossource = filepath2os(self.source) + opts = "filesrc location={ossource}" - if (os.path.splitext(self.source)[1]) == ".ts": - opts += " ! tsdemux" + opts += self.get_demuxer(self.source) opts += " ! " + self.get_decoder(self.codec, self.mode) + if self.mode in ["hw", "sw"]: opts += " ! 'video/x-raw(ANY),format={mformat}'" @@ -182,12 +191,12 @@ def transcode(self): get_media()._set_test_details(**{"output.{}".format(n) : output}) for channel in range(output.get("channels", 1)): encoded = self.goutputs[n][channel] - osencoded = filepath2os(encoded) - self.check_resolution(output, osencoded) - self.check_metrics(output, osencoded, refctx = [(n, channel)]) + self.check_resolution(output, encoded) + self.check_metrics(output, encoded, refctx = [(n, channel)]) def check_resolution(self, output, encoded): - props = [l.strip() for l in gst_discover(encoded).split('\n')] + osencoded = filepath2os(encoded) + props = [l.strip() for l in gst_discover(osencoded).split('\n')] width = output.get("width", self.width) height = output.get("height", self.height) @@ -197,13 +206,14 @@ def check_resolution(self, output, encoded): def check_metrics(self, output, encoded, refctx): ocodec = output["codec"] odecoder = self.get_decoder(ocodec, "hw") + osencoded = filepath2os(encoded) vppscale = self.get_vpp_scale(self.width, self.height, "hw") statsfile = get_media().artifacts.reserve("psnr") osstatsfile = filepath2os(statsfile) iopts = ( - f"filesrc location={encoded} ! {odecoder} ! {vppscale}" + f"filesrc location={osencoded} {self.get_demuxer(encoded)} ! {odecoder} ! {vppscale}" f" ! videorate ! 'video/x-raw(ANY),framerate={self.gstfps}' ! cmp." f" {self.gen_input_opts()} ! {vppscale} ! cmp." f" avvideocompare method=psnr stats-file={osstatsfile} name=cmp" diff --git a/lib/gstreamer/va/decoder.py b/lib/gstreamer/va/decoder.py index 0145f98e..eef45fe2 100644 --- a/lib/gstreamer/va/decoder.py +++ b/lib/gstreamer/va/decoder.py @@ -29,13 +29,14 @@ def before(self): def decode_test_class(codec, bitdepth, **kwargs): # caps lookup translation capcodec = codec - if codec in [Codec.HEVC, Codec.VP9, Codec.AV1]: + if codec in [Codec.HEVC, Codec.VP9, Codec.AV1, Codec.VVC]: capcodec = f"{codec}_{bitdepth}" # gst element codec translation gstcodec = { Codec.AVC : "h264", Codec.HEVC : "h265", + Codec.VVC : "h266", }.get(codec, codec) gstparser = { @@ -90,3 +91,7 @@ def validate_caps(self): ## MPEG2 ## MPEG2DecoderTest = decode_test_class(codec = Codec.MPEG2, bitdepth = 8) + +## VVC ## +VVC_8DecoderTest = decode_test_class(codec = Codec.VVC, bitdepth = 8) +VVC_10DecoderTest = decode_test_class(codec = Codec.VVC, bitdepth = 10) diff --git a/lib/gstreamer/va/transcoder.py b/lib/gstreamer/va/transcoder.py index 01272a86..37224038 100755 --- a/lib/gstreamer/va/transcoder.py +++ b/lib/gstreamer/va/transcoder.py @@ -27,9 +27,6 @@ def make_requirements(): sw = (dict(maxres = (16384, 16384)), have_gst_element("libde265dec"), "h265parse ! libde265dec ! videoconvert"), hw = (platform.get_caps("decode", "hevc_8"), have_gst_element(f"va{hw}h265dec"), f"h265parse ! va{hw}h265dec"), ), - Codec.AV1 : dict( - hw = (platform.get_caps("decode", "av1_8"), have_gst_element(f"va{hw}av1dec"), f"matroskademux ! av1parse ! va{hw}av1dec"), - ), Codec.MPEG2 : dict( sw = (dict(maxres = (2048, 2048)), have_gst_element("mpeg2dec"), "mpegvideoparse ! mpeg2dec ! videoconvert"), hw = (platform.get_caps("decode", "mpeg2"), have_gst_element(f"va{hw}mpeg2dec"), f"mpegvideoparse ! va{hw}mpeg2dec"), @@ -39,7 +36,10 @@ def make_requirements(): hw = (platform.get_caps("decode", "jpeg"), have_gst_element(f"va{hw}jpegdec"), f"jpegparse ! va{hw}jpegdec"), ), Codec.VP9 : dict( - hw = (platform.get_caps("decode", "vp9_8"), have_gst_element(f"va{hw}vp9dec"), f"matroskademux ! vp9parse ! va{hw}vp9dec"), + hw = (platform.get_caps("decode", "vp9_8"), have_gst_element(f"va{hw}vp9dec"), f"vp9parse ! va{hw}vp9dec"), + ), + Codec.AV1 : dict( + hw = (platform.get_caps("decode", "av1_8"), have_gst_element(f"va{hw}av1dec"), f"av1parse ! va{hw}av1dec"), ), }, encode = { @@ -53,9 +53,6 @@ def make_requirements(): hw = (platform.get_caps("encode", "hevc_8"), have_gst_element(f"va{hw}h265enc"), f"va{hw}h265enc ! video/x-h265,profile=main ! h265parse"), lp = (platform.get_caps("vdenc", "hevc_8"), have_gst_element(f"va{hw}h265lpenc"), f"va{hw}h265lpenc ! video/x-h265,profile=main ! h265parse"), ), - Codec.AV1 : dict( - lp = (platform.get_caps("vdenc", "av1_8"), have_gst_element(f"va{hw}av1lpenc"), f"va{hw}av1lpenc ! video/x-av1,profile=main ! av1parse ! matroskamux"), - ), Codec.MPEG2 : dict( sw = (dict(maxres = (2048, 2048)), have_gst_element("avenc_mpeg2video"), "avenc_mpeg2video ! mpegvideoparse"), hw = (platform.get_caps("encode", "mpeg2"), have_gst_element(f"va{hw}mpeg2enc"), f"va{hw}mpeg2enc ! mpegvideoparse"), @@ -67,6 +64,9 @@ def make_requirements(): Codec.VP9 : dict( lp = (platform.get_caps("vdenc", "vp9_8"), have_gst_element(f"va{hw}vp9lpenc"), f"va{hw}vp9lpenc ! vp9parse ! matroskamux"), ), + Codec.AV1 : dict( + lp = (platform.get_caps("vdenc", "av1_8"), have_gst_element(f"va{hw}av1lpenc"), f"va{hw}av1lpenc ! video/x-av1,profile=main ! av1parse ! matroskamux"), + ), }, vpp = { "scale" : dict( diff --git a/lib/gstreamer/vaapi/transcoder.py b/lib/gstreamer/vaapi/transcoder.py index 31f37788..3cef7554 100644 --- a/lib/gstreamer/vaapi/transcoder.py +++ b/lib/gstreamer/vaapi/transcoder.py @@ -46,7 +46,10 @@ class TranscoderTest(BaseTranscoderTest): ), ), Codec.VP9 : dict( - hw = (platform.get_caps("decode", "vp9_8"), have_gst_element(f"vaapivp9dec"), f"matroskademux ! vp9parse ! vaapivp9dec"), + hw = (platform.get_caps("decode", "vp9_8"), have_gst_element(f"vaapivp9dec"), f"vp9parse ! vaapivp9dec"), + ), + Codec.AV1 : dict( + hw = (platform.get_caps("decode", "av1_8"), have_gst_element("vaapiav1dec"), "av1parse ! vaapiav1dec"), ), }, encode = { diff --git a/requirements.txt b/requirements.txt index 6417f5db..4d46b247 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -slash>=1.12.0 +slash >= 1.12.0 numpy scikit-image distro diff --git a/test/ffmpeg-vaapi/decode/10bit/vvc.py b/test/ffmpeg-vaapi/decode/10bit/vvc.py new file mode 100755 index 00000000..8998083f --- /dev/null +++ b/test/ffmpeg-vaapi/decode/10bit/vvc.py @@ -0,0 +1,29 @@ +### +### Copyright (C) 2024 Intel Corporation +### +### SPDX-License-Identifier: BSD-3-Clause +### + +from .....lib import * +from .....lib.ffmpeg.vaapi.util import * +from .....lib.ffmpeg.vaapi.decoder import DecoderTest + +spec = load_test_spec("vvc", "decode", "10bit") + +@slash.requires(*platform.have_caps("decode", "vvc_10")) +@slash.requires(*have_ffmpeg_decoder("vvc")) +class default(DecoderTest): + def before(self): + vars(self).update( + caps = platform.get_caps("decode", "vvc_10"), + ffdecoder = "vvc", + metric = dict(type = "ssim", miny = 1.0, minu = 1.0, minv = 1.0), # default metric + strict = -2, # Currently, VVC decoder is experimental. Enable it with strict. + ) + super().before() + + @slash.parametrize(("case"), sorted(spec.keys())) + def test(self, case): + vars(self).update(spec[case].copy()) + self.case = case + self.decode() diff --git a/test/ffmpeg-vaapi/decode/vvc.py b/test/ffmpeg-vaapi/decode/vvc.py new file mode 100755 index 00000000..c9a3b5b6 --- /dev/null +++ b/test/ffmpeg-vaapi/decode/vvc.py @@ -0,0 +1,29 @@ +### +### Copyright (C) 2024 Intel Corporation +### +### SPDX-License-Identifier: BSD-3-Clause +### + +from ....lib import * +from ....lib.ffmpeg.vaapi.util import * +from ....lib.ffmpeg.vaapi.decoder import DecoderTest + +spec = load_test_spec("vvc", "decode", "8bit") + +@slash.requires(*platform.have_caps("decode", "vvc_8")) +@slash.requires(*have_ffmpeg_decoder("vvc")) +class default(DecoderTest): + def before(self): + vars(self).update( + caps = platform.get_caps("decode", "vvc_8"), + ffdecoder = "vvc", + metric = dict(type = "ssim", miny = 1.0, minu = 1.0, minv = 1.0), # default metric + strict = -2, # Currently, VVC decoder is experimental. Enable it with strict. + ) + super().before() + + @slash.parametrize(("case"), sorted(spec.keys())) + def test(self, case): + vars(self).update(spec[case].copy()) + self.case = case + self.decode() \ No newline at end of file diff --git a/test/gst-va/decode/10bit/vvc.py b/test/gst-va/decode/10bit/vvc.py new file mode 100755 index 00000000..5d2eff0c --- /dev/null +++ b/test/gst-va/decode/10bit/vvc.py @@ -0,0 +1,25 @@ +### +### Copyright (C) 2024 Intel Corporation +### +### SPDX-License-Identifier: BSD-3-Clause +### + +from .....lib import * +from .....lib.gstreamer.va.util import * +from .....lib.gstreamer.va.decoder import VVC_10DecoderTest as DecoderTest + +spec = load_test_spec("vvc", "decode", "10bit") + +class default(DecoderTest): + def before(self): + # default metric + vars(self).update( + metric = dict(type = "ssim", miny = 1.0, minu = 1.0, minv = 1.0), + ) + super().before() + + @slash.parametrize(("case"), sorted(spec.keys())) + def test(self, case): + vars(self).update(spec[case].copy()) + self.case = case + self.decode() diff --git a/test/gst-va/decode/vvc.py b/test/gst-va/decode/vvc.py new file mode 100755 index 00000000..71743036 --- /dev/null +++ b/test/gst-va/decode/vvc.py @@ -0,0 +1,25 @@ +### +### Copyright (C) 2024 Intel Corporation +### +### SPDX-License-Identifier: BSD-3-Clause +### + +from ....lib import * +from ....lib.gstreamer.va.util import * +from ....lib.gstreamer.va.decoder import VVC_8DecoderTest as DecoderTest + +spec = load_test_spec("vvc", "decode", "8bit") + +class default(DecoderTest): + def before(self): + # default metric + vars(self).update( + metric = dict(type = "ssim", miny = 1.0, minu = 1.0, minv = 1.0), + ) + super().before() + + @slash.parametrize(("case"), sorted(spec.keys())) + def test(self, case): + vars(self).update(spec[case].copy()) + self.case = case + self.decode() diff --git a/test/gst-vaapi/transcode/av1.py b/test/gst-vaapi/transcode/av1.py new file mode 100644 index 00000000..acebb2ab --- /dev/null +++ b/test/gst-vaapi/transcode/av1.py @@ -0,0 +1,23 @@ +### +### Copyright (C) 2024 Intel Corporation +### +### SPDX-License-Identifier: BSD-3-Clause +### + +from ....lib import * +from ....lib.gstreamer.vaapi.util import * +from ....lib.gstreamer.vaapi.transcoder import TranscoderTest +from ....lib.codecs import Codec + +spec = load_test_spec("av1", "transcode") + +class default(TranscoderTest): + @slash.parametrize(("case"), sorted_by_resolution(spec)) + def test(self, case): + vars(self).update(spec[case].copy()) + vars(self).update( + case = case, + codec = Codec.AV1, + ) + self.transcode() +