Skip to content

Commit

Permalink
[GStreamer][WebCodecs] HEVC encoding and decoding support
Browse files Browse the repository at this point in the history
https://bugs.webkit.org/show_bug.cgi?id=260124

Reviewed by Xabier Rodriguez-Calvar.

The corresponding layout tests are now passing, excepted the h265_annexb variants, that would
require further investigation. Support for H265 codec string to GStreamer H265 profile was also
added, with unit-tests.

* LayoutTests/platform/glib/TestExpectations:
* Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml:
* Source/WebCore/platform/GStreamer.cmake:
* Source/WebCore/platform/graphics/gstreamer/GStreamerCommon.cpp:
(WebCore::configureVideoDecoderForHarnessing):
(WebCore::wrapSpanData):
* Source/WebCore/platform/graphics/gstreamer/GStreamerCommon.h:
* Source/WebCore/platform/graphics/gstreamer/GStreamerRegistryScanner.cpp:
(WebCore::GStreamerRegistryScanner::initializeEncoders):
* Source/WebCore/platform/graphics/gstreamer/VideoDecoderGStreamer.cpp:
(WebCore::GStreamerInternalVideoDecoder::GStreamerInternalVideoDecoder):
* Source/WebCore/platform/graphics/gstreamer/VideoEncoderGStreamer.cpp:
(WebCore::GStreamerInternalVideoEncoder::initialize):
* Source/WebCore/platform/gstreamer/GStreamerCodecUtilities.cpp:
(WebCore::GStreamerCodecUtilities::parseHEVCProfile):
* Source/WebCore/platform/gstreamer/GStreamerCodecUtilities.h:
* Source/WebCore/platform/gstreamer/VideoEncoderPrivateGStreamer.cpp:
(webkit_video_encoder_class_init):
* Tools/TestWebKitAPI/Tests/WebCore/gstreamer/GStreamerTest.cpp:
(TestWebKitAPI::TEST_F):

Canonical link: https://commits.webkit.org/266904@main
  • Loading branch information
philn committed Aug 15, 2023
1 parent 5caa080 commit 904e4fc
Show file tree
Hide file tree
Showing 12 changed files with 237 additions and 21 deletions.
16 changes: 8 additions & 8 deletions LayoutTests/platform/glib/TestExpectations
Original file line number Diff line number Diff line change
Expand Up @@ -1130,14 +1130,14 @@ imported/w3c/web-platform-tests/webcodecs/temporal-svc-encoding.https.any.html?v
imported/w3c/web-platform-tests/webcodecs/temporal-svc-encoding.https.any.html?vp9 [ Failure ]

# HEVC support
imported/w3c/web-platform-tests/webcodecs/full-cycle-test.https.any.html?h265_annexb [ Failure ]
imported/w3c/web-platform-tests/webcodecs/full-cycle-test.https.any.html?h265_hevc [ Failure ]
imported/w3c/web-platform-tests/webcodecs/full-cycle-test.https.any.worker.html?h265_annexb [ Failure ]
imported/w3c/web-platform-tests/webcodecs/full-cycle-test.https.any.worker.html?h265_hevc [ Failure ]
imported/w3c/web-platform-tests/webcodecs/videoDecoder-codec-specific.https.any.html?h265_annexb [ Skip ]
imported/w3c/web-platform-tests/webcodecs/videoDecoder-codec-specific.https.any.html?h265_hevc [ Skip ]
imported/w3c/web-platform-tests/webcodecs/videoDecoder-codec-specific.https.any.worker.html?h265_annexb [ Skip ]
imported/w3c/web-platform-tests/webcodecs/videoDecoder-codec-specific.https.any.worker.html?h265_hevc [ Skip ]
imported/w3c/web-platform-tests/webcodecs/full-cycle-test.https.any.html?h265_annexb [ Pass ]
imported/w3c/web-platform-tests/webcodecs/full-cycle-test.https.any.html?h265_hevc [ Pass ]
imported/w3c/web-platform-tests/webcodecs/full-cycle-test.https.any.worker.html?h265_annexb [ Pass ]
imported/w3c/web-platform-tests/webcodecs/full-cycle-test.https.any.worker.html?h265_hevc [ Pass ]
imported/w3c/web-platform-tests/webcodecs/videoDecoder-codec-specific.https.any.html?h265_annexb [ Failure ]
imported/w3c/web-platform-tests/webcodecs/videoDecoder-codec-specific.https.any.html?h265_hevc [ Pass ]
imported/w3c/web-platform-tests/webcodecs/videoDecoder-codec-specific.https.any.worker.html?h265_annexb [ Failure ]
imported/w3c/web-platform-tests/webcodecs/videoDecoder-codec-specific.https.any.worker.html?h265_hevc [ Pass ]

imported/w3c/web-platform-tests/webcodecs/videoFrame-createImageBitmap.any.worker.html [ Pass ]

Expand Down
2 changes: 2 additions & 0 deletions Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7396,8 +7396,10 @@ WebCodecsHEVCEnabled:
WebKitLegacy:
default: false
WebKit:
"USE(GSTREAMER)": true
default: false
WebCore:
"USE(GSTREAMER)": true
default: false

WebCryptoSafeCurvesEnabled:
Expand Down
1 change: 1 addition & 0 deletions Source/WebCore/platform/GStreamer.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ if (ENABLE_VIDEO OR ENABLE_WEB_AUDIO)
)

list(APPEND WebCore_PRIVATE_FRAMEWORK_HEADERS
platform/gstreamer/GStreamerCodecUtilities.h
platform/gstreamer/GStreamerElementHarness.h
platform/graphics/gstreamer/GRefPtrGStreamer.h
platform/graphics/gstreamer/GStreamerCommon.h
Expand Down
20 changes: 20 additions & 0 deletions Source/WebCore/platform/graphics/gstreamer/GStreamerCommon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,12 @@ void configureVideoDecoderForHarnessing(const GRefPtr<GstElement>& element)

if (gstObjectHasProperty(element.get(), "max-errors"))
g_object_set(element.get(), "max-errors", 0, nullptr);

if (gstObjectHasProperty(element.get(), "std-compliance"))
gst_util_set_object_arg(G_OBJECT(element.get()), "std-compliance", "strict");

if (gstObjectHasProperty(element.get(), "output-corrupt"))
g_object_set(element.get(), "output-corrupt", FALSE, nullptr);
}

static bool gstObjectHasProperty(GstObject* gstObject, const char* name)
Expand All @@ -1048,6 +1054,20 @@ bool gstObjectHasProperty(GstPad* pad, const char* name)
return gstObjectHasProperty(GST_OBJECT_CAST(pad), name);
}

GRefPtr<GstBuffer> wrapSpanData(const std::span<const uint8_t>& span)
{
if (span.empty())
return nullptr;

Vector<uint8_t> data { span };
auto bufferSize = data.size();
auto bufferData = data.data();
auto buffer = adoptGRef(gst_buffer_new_wrapped_full(GST_MEMORY_FLAG_READONLY, bufferData, bufferSize, 0, bufferSize, new Vector<uint8_t>(WTFMove(data)), [](gpointer data) {
delete static_cast<Vector<uint8_t>*>(data);
}));
return buffer;
}

#undef GST_CAT_DEFAULT

} // namespace WebCore
Expand Down
2 changes: 2 additions & 0 deletions Source/WebCore/platform/graphics/gstreamer/GStreamerCommon.h
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,8 @@ void configureVideoDecoderForHarnessing(const GRefPtr<GstElement>&);
bool gstObjectHasProperty(GstElement*, const char* name);
bool gstObjectHasProperty(GstPad*, const char* name);

GRefPtr<GstBuffer> wrapSpanData(const std::span<const uint8_t>&);

} // namespace WebCore

#ifndef GST_BUFFER_DTS_OR_PTS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,12 @@ void GStreamerRegistryScanner::initializeEncoders(const GStreamerRegistryScanner
m_encoderCodecMap.add(AtomString("mp4v*"_s), h264EncoderAvailable);
}

auto h265EncoderAvailable = factories.hasElementForMediaType(ElementFactories::Type::VideoEncoder, "video/x-h265, profile=(string){ main, high }", ElementFactories::CheckHardwareClassifier::Yes);
if (h265EncoderAvailable) {
m_encoderCodecMap.add(AtomString("hev1*"_s), h265EncoderAvailable);
m_encoderCodecMap.add(AtomString("hvc1*"_s), h265EncoderAvailable);
}

if (factories.hasElementForMediaType(ElementFactories::Type::Muxer, "video/quicktime")) {
if (opusSupported)
m_encoderMimeTypeSet.add(AtomString("audio/opus"_s));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,17 +143,8 @@ GStreamerInternalVideoDecoder::GStreamerInternalVideoDecoder(const String& codec
if (codecName.startsWith("avc1"_s)) {
inputCaps = adoptGRef(gst_caps_new_simple("video/x-h264", "stream-format", G_TYPE_STRING, "avc", "alignment", G_TYPE_STRING, "au", nullptr));
parser = "h264parse";

Vector<uint8_t> data { config.description };
if (!data.isEmpty()) {
auto bufferSize = data.size();
auto bufferData = data.data();
auto* codecData = gst_buffer_new_wrapped_full(GST_MEMORY_FLAG_READONLY, bufferData, bufferSize, 0, bufferSize, new Vector<uint8_t>(WTFMove(data)), [](gpointer data) {
delete static_cast<Vector<uint8_t>*>(data);
});

gst_caps_set_simple(inputCaps.get(), "codec_data", GST_TYPE_BUFFER, codecData, nullptr);
}
if (auto codecData = wrapSpanData(config.description))
gst_caps_set_simple(inputCaps.get(), "codec_data", GST_TYPE_BUFFER, codecData.get(), nullptr);
} else if (codecName.startsWith("av01"_s)) {
inputCaps = adoptGRef(gst_caps_new_simple("video/x-av1", "stream-format", G_TYPE_STRING, "obu-stream", "alignment", G_TYPE_STRING, "frame", nullptr));
parser = "av1parse";
Expand All @@ -162,6 +153,16 @@ GStreamerInternalVideoDecoder::GStreamerInternalVideoDecoder(const String& codec
else if (codecName.startsWith("vp09"_s)) {
inputCaps = adoptGRef(gst_caps_new_empty_simple("video/x-vp9"));
parser = "vp9parse";
} else if (codecName.startsWith("hvc1"_s)) {
inputCaps = adoptGRef(gst_caps_new_simple("video/x-h265", "stream-format", G_TYPE_STRING, "hvc1", "alignment", G_TYPE_STRING, "au", nullptr));
parser = "h265parse";
if (auto codecData = wrapSpanData(config.description))
gst_caps_set_simple(inputCaps.get(), "codec_data", GST_TYPE_BUFFER, codecData.get(), nullptr);
} else if (codecName.startsWith("hev1"_s)) {
inputCaps = adoptGRef(gst_caps_new_simple("video/x-h265", "stream-format", G_TYPE_STRING, "hev1", "alignment", G_TYPE_STRING, "au", nullptr));
parser = "h265parse";
if (auto codecData = wrapSpanData(config.description))
gst_caps_set_simple(inputCaps.get(), "codec_data", GST_TYPE_BUFFER, codecData.get(), nullptr);
} else {
WTFLogAlways("Codec %s not wired in yet", codecName.ascii().data());
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,12 @@ String GStreamerInternalVideoEncoder::initialize(const VideoEncoder::Config& con
} else if (m_codecName.startsWith("av01"_s)) {
// FIXME: parse codec parameters.
encoderCaps = adoptGRef(gst_caps_new_empty_simple("video/x-av1"));
} else if (m_codecName.startsWith("hvc1"_s) || m_codecName.startsWith("hev1"_s)) {
encoderCaps = adoptGRef(gst_caps_new_empty_simple("video/x-h265"));
if (const char* profile = GStreamerCodecUtilities::parseHEVCProfile(m_codecName))
gst_caps_set_simple(encoderCaps.get(), "profile", G_TYPE_STRING, profile, nullptr);
} else
return makeString("Unsupported outgoing video encoding: ", m_codecName);
return makeString("Unsupported outgoing video encoding: "_s, m_codecName);

if (config.width)
gst_caps_set_simple(encoderCaps.get(), "width", G_TYPE_INT, static_cast<int>(config.width), nullptr);
Expand Down
25 changes: 25 additions & 0 deletions Source/WebCore/platform/gstreamer/GStreamerCodecUtilities.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

#if USE(GSTREAMER)

#include "HEVCUtilities.h"
#include <gst/pbutils/codec-utils.h>
#include <wtf/text/StringToIntegerConversion.h>
#include <wtf/text/WTFString.h>
Expand Down Expand Up @@ -66,6 +67,30 @@ std::pair<const char*, const char*> GStreamerCodecUtilities::parseH264ProfileAnd
return { profile, level };
}

const char* GStreamerCodecUtilities::parseHEVCProfile(const String& codec)
{
ensureDebugCategoryInitialized();

GST_DEBUG("Parsing HEVC codec string: %s", codec.ascii().data());
auto parameters = parseHEVCCodecParameters(codec);
if (!parameters) {
GST_WARNING("Invalid HEVC codec: %s", codec.ascii().data());
return nullptr;
}

uint8_t profileTierLevel[11] = { 0, };
memset(profileTierLevel, 0, 11);
profileTierLevel[0] = parameters->generalProfileIDC;

if (profileTierLevel[0] >= 4) {
auto& constraints = parameters->generalConstraintIndicatorFlags;
for (unsigned i = 5, j = 0; i < 10; i++, j++)
profileTierLevel[i] = constraints[j];
}

return gst_codec_utils_h265_get_profile(profileTierLevel, sizeof(profileTierLevel));
}

uint8_t GStreamerCodecUtilities::parseVP9Profile(const String& codec)
{
ensureDebugCategoryInitialized();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ namespace WebCore {
namespace GStreamerCodecUtilities {

std::pair<const char*, const char*> parseH264ProfileAndLevel(const String& codec);
const char* parseHEVCProfile(const String& codec);
uint8_t parseVP9Profile(const String& codec);

} // namespace GStreamerCodecUtilities
Expand Down
117 changes: 117 additions & 0 deletions Source/WebCore/platform/gstreamer/VideoEncoderPrivateGStreamer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ struct EncoderDefinition {
enum EncoderId {
None,
X264,
X265,
OpenH264,
OmxH264,
VaapiH264,
Expand Down Expand Up @@ -840,6 +841,122 @@ static void webkit_video_encoder_class_init(WebKitVideoEncoderClass* klass)
});
}

static GQuark x265BitrateQuark = g_quark_from_static_string("x265-bitrate-mode");
Encoders::registerEncoder(X265, "x265enc", "h265parse", "video/x-h265",
"video/x-h265,alignment=au,stream-format=byte-stream",
[](WebKitVideoEncoder* self) {
g_object_set(self->priv->encoder.get(), "key-int-max", 15, nullptr);

const auto* structure = gst_caps_get_structure(self->priv->encodedCaps.get(), 0);
auto inputCaps = adoptGRef(gst_caps_new_any());
if (const char* profileString = gst_structure_get_string(structure, "profile")) {
const char* pixelFormat = nullptr;
auto pad = adoptGRef(gst_element_get_static_pad(self->priv->encoder.get(), "sink"));
auto allowedCaps = adoptGRef(gst_pad_query_caps(pad.get(), nullptr));
const auto* structure = gst_caps_get_structure(allowedCaps.get(), 0);
const auto* formatValue = gst_structure_get_value(structure, "format");
unsigned size = gst_value_list_get_size(formatValue);
bool supports10BitsLittleEndian = false;
bool supports10BitsBigEndian = false;
bool supports12BitsLittleEndian = false;
bool supports12BitsBigEndian = false;

for (unsigned i = 0; i < size; i++) {
auto* value = gst_value_list_get_value(formatValue, i);
const char* format = g_value_get_string(value);
if (g_str_has_suffix(format, "_10LE"))
supports10BitsLittleEndian = true;
if (g_str_has_suffix(format, "_10BE"))
supports10BitsBigEndian = true;
if (g_str_has_suffix(format, "_12LE"))
supports12BitsLittleEndian = true;
if (g_str_has_suffix(format, "_12BE"))
supports12BitsBigEndian = true;
}

StringView profile { profileString, static_cast<unsigned>(strlen(profileString)) };
auto is12Bits = profile.findIgnoringASCIICase("-12"_s) != notFound;
auto is10Bits = profile.findIgnoringASCIICase("-10"_s) != notFound;
auto isY444 = profile.findIgnoringASCIICase("-444"_s) != notFound;
auto isY422 = profile.findIgnoringASCIICase("-422"_s) != notFound;

if (is12Bits) {
if (isY444) {
if (supports12BitsLittleEndian)
pixelFormat = "Y444_12LE";
else if (supports12BitsBigEndian)
pixelFormat = "Y444_12BE";
else
pixelFormat = "Y444";
} else if (isY422) {
if (supports12BitsLittleEndian)
pixelFormat = "Y422_12LE";
else if (supports12BitsBigEndian)
pixelFormat = "Y422_12BE";
else
pixelFormat = "Y42B";
}
} else if (is10Bits) {
if (isY444) {
if (supports10BitsLittleEndian)
pixelFormat = "Y444_10LE";
else if (supports10BitsBigEndian)
pixelFormat = "Y444_10BE";
else
pixelFormat = "Y444";
} else if (isY422) {
if (supports10BitsLittleEndian)
pixelFormat = "Y422_10LE";
else if (supports10BitsBigEndian)
pixelFormat = "Y422_10BE";
else
pixelFormat = "Y42B";
} else if (profile == "high-10"_s) {
if (supports10BitsLittleEndian)
pixelFormat = "Y420_10LE";
else if (supports10BitsBigEndian)
pixelFormat = "Y420_10BE";
}
} else
pixelFormat = "I420";

GST_DEBUG("Setting pixel format %s for profile %s", pixelFormat, profileString);
if (pixelFormat)
inputCaps = adoptGRef(gst_caps_new_simple("video/x-raw", "format", G_TYPE_STRING, pixelFormat, nullptr));
}
g_object_set(self->priv->inputCapsFilter.get(), "caps", inputCaps.get(), nullptr);
g_object_set(self->priv->outputCapsFilter.get(), "caps", self->priv->encodedCaps.get(), nullptr);
}, "bitrate", [](GObject* object, const char* propertyName, int bitrate) {
if (UNLIKELY(!bitrate))
return;
setBitrateKbitPerSec(object, propertyName, bitrate);
auto bitrateMode = GPOINTER_TO_INT(g_object_get_qdata(object, x265BitrateQuark));
StringBuilder builder;
switch (bitrateMode) {
case CONSTANT_BITRATE_MODE:
builder.append("vbv-maxrate="_s, bitrate, ":vbv-bufsize="_s, bitrate / 2);
break;
case VARIABLE_BITRATE_MODE:
builder.append("vbv-maxrate=0:vbvbufsize=0"_s);
break;
};
auto options = builder.toString();
g_object_set(object, "option-string", options.ascii().data(), nullptr);
}, "key-int-max", [](GstElement* encoder, BitrateMode mode) {
g_object_set_qdata(G_OBJECT(encoder), x265BitrateQuark, GINT_TO_POINTER(mode));
}, [](GstElement* encoder, LatencyMode mode) {
switch (mode) {
case REALTIME_LATENCY_MODE:
gst_util_set_object_arg(G_OBJECT(encoder), "tune", "zerolatency");
gst_util_set_object_arg(G_OBJECT(encoder), "speed-preset", "ultrafast");
break;
case QUALITY_LATENCY_MODE:
g_object_set(encoder, "tune", 0, nullptr);
gst_util_set_object_arg(G_OBJECT(encoder), "speed-preset", "No preset");
break;
};
});

auto srcPadTemplateCaps = createSrcPadTemplateCaps();
gst_element_class_add_pad_template(elementClass, gst_pad_template_new("src", GST_PAD_SRC, GST_PAD_ALWAYS, srcPadTemplateCaps.get()));

Expand Down
39 changes: 38 additions & 1 deletion Tools/TestWebKitAPI/Tests/WebCore/gstreamer/GStreamerTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
#if USE(GSTREAMER)
#include "GStreamerTest.h"

#include <WebCore/GStreamerCodecUtilities.h>
#include <WebCore/GStreamerCommon.h>
#include <gst/gst.h>

using namespace WebCore;

Expand Down Expand Up @@ -66,6 +66,43 @@ TEST_F(GStreamerTest, gstStructureJSONSerializing)
ASSERT_EQ(jsonString, "{\"words\":[\"hello\",\"world\"]}"_s);
}

TEST_F(GStreamerTest, codecStringParsing)
{
using namespace GStreamerCodecUtilities;

ASSERT_STREQ(parseHEVCProfile("hev1.1.6.L93.B0"_s), "main");
ASSERT_STREQ(parseHEVCProfile("hev1.2.4.L93.B0"_s), "main-10");
ASSERT_STREQ(parseHEVCProfile("hev1.3.E.L93.B0"_s), "main-still-picture");
ASSERT_STREQ(parseHEVCProfile("hev1.4.10.L186.BF.C8"_s), "monochrome");
ASSERT_STREQ(parseHEVCProfile("hev1.4.10.L30.BD.C8"_s), "monochrome-10");
ASSERT_STREQ(parseHEVCProfile("hev1.4.10.L30.B9.C8"_s), "monochrome-12");
ASSERT_STREQ(parseHEVCProfile("hev1.4.10.L30.B1.C8"_s), "monochrome-16");
ASSERT_STREQ(parseHEVCProfile("hev1.4.10.L30.B9.88"_s), "main-12");

ASSERT_STREQ(parseHEVCProfile("hev1.4.10.L30.BE.08"_s), "main-444");
ASSERT_STREQ(parseHEVCProfile("hev1.4.10.L30.BC.08"_s), "main-444-10");
ASSERT_STREQ(parseHEVCProfile("hev1.4.10.L30.B8.08"_s), "main-444-12");

ASSERT_STREQ(parseHEVCProfile("hev1.4.10.L30.BF.A8"_s), "main-intra");
ASSERT_STREQ(parseHEVCProfile("hev1.4.10.L30.BD.A8"_s), "main-10-intra");
ASSERT_STREQ(parseHEVCProfile("hev1.4.10.L30.B9.A8"_s), "main-12-intra");

ASSERT_STREQ(parseHEVCProfile("hev1.4.10.L30.BE.28"_s), "main-444-intra");
ASSERT_STREQ(parseHEVCProfile("hev1.4.10.L60.BC.28"_s), "main-444-10-intra");
ASSERT_STREQ(parseHEVCProfile("hev1.4.10.L30.B0.20"_s), "main-444-16-intra");

ASSERT_STREQ(parseHEVCProfile("hev1.5.20.L30.BE.0C"_s), "high-throughput-444");
ASSERT_STREQ(parseHEVCProfile("hev1.5.20.L30.BC.0C"_s), "high-throughput-444-10");
ASSERT_STREQ(parseHEVCProfile("hev1.5.20.L30.B0.0C"_s), "high-throughput-444-14");
ASSERT_STREQ(parseHEVCProfile("hev1.5.20.L30.B0.24"_s), "high-throughput-444-16-intra");

ASSERT_STREQ(parseHEVCProfile("hev1.9.200.L30.BF.8C"_s), "screen-extended-main");
ASSERT_STREQ(parseHEVCProfile("hev1.9.200.L30.BD.8C"_s), "screen-extended-main-10");
ASSERT_STREQ(parseHEVCProfile("hev1.9.200.L30.BE.0C"_s), "screen-extended-main-444");
ASSERT_STREQ(parseHEVCProfile("hev1.9.200.L30.BC.0C"_s), "screen-extended-main-444-10");
ASSERT_STREQ(parseHEVCProfile("hev1.9.200.L30.B0.0C"_s), "screen-extended-high-throughput-444-14");
}

} // namespace TestWebKitAPI

#endif // USE(GSTREAMER)

0 comments on commit 904e4fc

Please sign in to comment.