mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2025-03-30 12:49:40 +00:00
x264enc: Respect Youtube bitrate recommandation
Properly follow google recommendations[0] concerning bitrate when the user wants to use the youtube profile. [0]: https://support.google.com/youtube/answer/1722171?hl=en
This commit is contained in:
parent
a47b6b5f3c
commit
9273903286
5 changed files with 329 additions and 12 deletions
221
ext/x264/gstencoderbitrateprofilemanager.c
Normal file
221
ext/x264/gstencoderbitrateprofilemanager.c
Normal file
|
@ -0,0 +1,221 @@
|
|||
/* GStreamer
|
||||
* Copyright (C) 2019 Thibault Saunier <tsaunier@igalia.com>
|
||||
*
|
||||
* gstencoderbitrateprofilemanager.c
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
* Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
#include "gstencoderbitrateprofilemanager.h"
|
||||
|
||||
GST_DEBUG_CATEGORY_STATIC (encoderbitratemanager_debug);
|
||||
#define GST_CAT_DEFAULT encoderbitratemanager_debug
|
||||
|
||||
typedef struct
|
||||
{
|
||||
gchar *name;
|
||||
gsize n_vals;
|
||||
GstEncoderBitrateTargetForPixelsMap *map;
|
||||
} GstEncoderBitrateProfile;
|
||||
|
||||
struct _GstEncoderBitrateProfileManager
|
||||
{
|
||||
GList *profiles;
|
||||
gchar *preset;
|
||||
guint bitrate;
|
||||
|
||||
gboolean setting_preset;
|
||||
gboolean user_bitrate;
|
||||
};
|
||||
|
||||
/* *INDENT-OFF* */
|
||||
/* Copied from https://support.google.com/youtube/answer/1722171?hl=en */
|
||||
static const GstEncoderBitrateTargetForPixelsMap youtube_bitrate_profiles[] = {
|
||||
{
|
||||
.n_pixels = 3840 * 2160,
|
||||
.low_framerate_bitrate = 40000,
|
||||
.high_framerate_bitrate = 60000,
|
||||
},
|
||||
{
|
||||
.n_pixels = 2560 * 1440,
|
||||
.low_framerate_bitrate = 16000,
|
||||
.high_framerate_bitrate = 24000,
|
||||
},
|
||||
{
|
||||
.n_pixels = 1920 * 1080,
|
||||
.low_framerate_bitrate = 8000,
|
||||
.high_framerate_bitrate = 12000,
|
||||
},
|
||||
{
|
||||
.n_pixels = 1080 * 720,
|
||||
.low_framerate_bitrate = 5000,
|
||||
.high_framerate_bitrate = 7500,
|
||||
},
|
||||
{
|
||||
.n_pixels = 640 * 480,
|
||||
.low_framerate_bitrate = 2500,
|
||||
.high_framerate_bitrate = 4000,
|
||||
},
|
||||
{
|
||||
.n_pixels = 0,
|
||||
.low_framerate_bitrate = 2500,
|
||||
.high_framerate_bitrate = 4000,
|
||||
},
|
||||
{
|
||||
.n_pixels = 0,
|
||||
.low_framerate_bitrate = 0,
|
||||
.high_framerate_bitrate = 0,
|
||||
},
|
||||
};
|
||||
/* *INDENT-ON* */
|
||||
|
||||
static void
|
||||
gst_encoder_bitrate_profile_free (GstEncoderBitrateProfile * profile)
|
||||
{
|
||||
g_free (profile->name);
|
||||
g_free (profile->map);
|
||||
g_free (profile);
|
||||
}
|
||||
|
||||
void
|
||||
gst_encoder_bitrate_profile_manager_add_profile (GstEncoderBitrateProfileManager
|
||||
* self, const gchar * profile_name,
|
||||
const GstEncoderBitrateTargetForPixelsMap * map)
|
||||
{
|
||||
gint n_vals;
|
||||
GstEncoderBitrateProfile *profile;
|
||||
|
||||
for (n_vals = 0;
|
||||
map[n_vals].low_framerate_bitrate != 0
|
||||
&& map[n_vals].high_framerate_bitrate != 0; n_vals++);
|
||||
n_vals++;
|
||||
|
||||
profile = g_new0 (GstEncoderBitrateProfile, 1);
|
||||
profile->name = g_strdup (profile_name);
|
||||
profile->n_vals = n_vals;
|
||||
profile->map
|
||||
= g_memdup (map, sizeof (GstEncoderBitrateTargetForPixelsMap) * n_vals);
|
||||
self->profiles = g_list_prepend (self->profiles, profile);
|
||||
}
|
||||
|
||||
guint
|
||||
gst_encoder_bitrate_profile_manager_get_bitrate (GstEncoderBitrateProfileManager
|
||||
* self, GstVideoInfo * info)
|
||||
{
|
||||
gint i;
|
||||
gboolean high_fps;
|
||||
guint num_pix;
|
||||
GList *tmp;
|
||||
|
||||
GstEncoderBitrateProfile *profile = NULL;
|
||||
|
||||
g_return_val_if_fail (self != NULL, -1);
|
||||
|
||||
if (!info || info->finfo == NULL
|
||||
|| info->finfo->format == GST_VIDEO_FORMAT_UNKNOWN) {
|
||||
GST_INFO ("Video info %p not usable, returning current bitrate", info);
|
||||
return self->bitrate;
|
||||
}
|
||||
|
||||
if (!self->preset) {
|
||||
GST_INFO ("No preset used, returning current bitrate");
|
||||
return self->bitrate;
|
||||
|
||||
}
|
||||
|
||||
for (tmp = self->profiles; tmp; tmp = tmp->next) {
|
||||
GstEncoderBitrateProfile *tmpprof = tmp->data;
|
||||
if (!g_strcmp0 (tmpprof->name, self->preset)) {
|
||||
profile = tmpprof;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!profile) {
|
||||
GST_INFO ("Could not find map for profile: %s", self->preset);
|
||||
|
||||
return self->bitrate;
|
||||
}
|
||||
|
||||
high_fps = GST_VIDEO_INFO_FPS_N (info) / GST_VIDEO_INFO_FPS_D (info) > 30.0;
|
||||
num_pix = GST_VIDEO_INFO_WIDTH (info) * GST_VIDEO_INFO_HEIGHT (info);
|
||||
for (i = 0; i < profile->n_vals; i++) {
|
||||
GstEncoderBitrateTargetForPixelsMap *bitrate_values = &profile->map[i];
|
||||
|
||||
if (num_pix < bitrate_values->n_pixels)
|
||||
continue;
|
||||
|
||||
self->bitrate =
|
||||
high_fps ? bitrate_values->
|
||||
high_framerate_bitrate : bitrate_values->low_framerate_bitrate;
|
||||
GST_INFO ("Using %s bitrate! %d", self->preset, self->bitrate);
|
||||
return self->bitrate;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
void gst_encoder_bitrate_profile_manager_start_loading_preset
|
||||
(GstEncoderBitrateProfileManager * self)
|
||||
{
|
||||
self->setting_preset = TRUE;
|
||||
}
|
||||
|
||||
void gst_encoder_bitrate_profile_manager_end_loading_preset
|
||||
(GstEncoderBitrateProfileManager * self, const gchar * preset)
|
||||
{
|
||||
self->setting_preset = FALSE;
|
||||
g_free (self->preset);
|
||||
self->preset = g_strdup (preset);
|
||||
}
|
||||
|
||||
void
|
||||
gst_encoder_bitrate_profile_manager_set_bitrate (GstEncoderBitrateProfileManager
|
||||
* self, guint bitrate)
|
||||
{
|
||||
self->bitrate = bitrate;
|
||||
self->user_bitrate = !self->setting_preset;
|
||||
}
|
||||
|
||||
void
|
||||
gst_encoder_bitrate_profile_manager_free (GstEncoderBitrateProfileManager *
|
||||
self)
|
||||
{
|
||||
g_free (self->preset);
|
||||
g_list_free_full (self->profiles,
|
||||
(GDestroyNotify) gst_encoder_bitrate_profile_free);
|
||||
g_free (self);
|
||||
}
|
||||
|
||||
GstEncoderBitrateProfileManager *
|
||||
gst_encoder_bitrate_profile_manager_new (guint default_bitrate)
|
||||
{
|
||||
GstEncoderBitrateProfileManager *self =
|
||||
g_new0 (GstEncoderBitrateProfileManager, 1);
|
||||
static volatile gsize _init = 0;
|
||||
|
||||
if (g_once_init_enter (&_init)) {
|
||||
GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "encoderbitratemanager", 0,
|
||||
"Encoder bitrate manager");
|
||||
g_once_init_leave (&_init, 1);
|
||||
}
|
||||
|
||||
self->bitrate = default_bitrate;
|
||||
gst_encoder_bitrate_profile_manager_add_profile (self,
|
||||
"Profile YouTube", youtube_bitrate_profiles);
|
||||
|
||||
return self;
|
||||
}
|
46
ext/x264/gstencoderbitrateprofilemanager.h
Normal file
46
ext/x264/gstencoderbitrateprofilemanager.h
Normal file
|
@ -0,0 +1,46 @@
|
|||
/* GStreamer
|
||||
* Copyright (C) 2019 Thibault Saunier <tsaunier@igalia.com>
|
||||
*
|
||||
* gstencoderbitrateprofilemanager.h
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
* Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <gst/gst.h>
|
||||
#include <gst/video/video.h>
|
||||
|
||||
typedef struct _GstEncoderBitrateProfileManager GstEncoderBitrateProfileManager;
|
||||
|
||||
typedef struct _GstEncoderBitrateTargetForPixelsMap
|
||||
{
|
||||
guint n_pixels;
|
||||
guint low_framerate_bitrate;
|
||||
guint high_framerate_bitrate;
|
||||
|
||||
gpointer _gst_reserved[GST_PADDING_LARGE];
|
||||
} GstEncoderBitrateTargetForPixelsMap;
|
||||
|
||||
void
|
||||
gst_encoder_bitrate_profile_manager_add_profile(GstEncoderBitrateProfileManager* self,
|
||||
const gchar* profile_name, const GstEncoderBitrateTargetForPixelsMap* map);
|
||||
guint gst_encoder_bitrate_profile_manager_get_bitrate(GstEncoderBitrateProfileManager* self, GstVideoInfo* info);
|
||||
void gst_encoder_bitrate_profile_manager_start_loading_preset (GstEncoderBitrateProfileManager* self);
|
||||
void gst_encoder_bitrate_profile_manager_end_loading_preset(GstEncoderBitrateProfileManager* self, const gchar* preset);
|
||||
void gst_encoder_bitrate_profile_manager_set_bitrate(GstEncoderBitrateProfileManager* self, guint bitrate);
|
||||
GstEncoderBitrateProfileManager* gst_encoder_bitrate_profile_manager_new(guint default_bitrate);
|
||||
void gst_encoder_bitrate_profile_manager_free(GstEncoderBitrateProfileManager* self);
|
|
@ -724,9 +724,36 @@ static void gst_x264_enc_set_property (GObject * object, guint prop_id,
|
|||
static void gst_x264_enc_get_property (GObject * object, guint prop_id,
|
||||
GValue * value, GParamSpec * pspec);
|
||||
|
||||
typedef gboolean (*LoadPresetFunc) (GstPreset * preset, const gchar * name);
|
||||
|
||||
LoadPresetFunc parent_load_preset = NULL;
|
||||
|
||||
static gboolean
|
||||
gst_x264_enc_load_preset (GstPreset * preset, const gchar * name)
|
||||
{
|
||||
GstX264Enc *enc = GST_X264_ENC (preset);
|
||||
gboolean res;
|
||||
|
||||
gst_encoder_bitrate_profile_manager_start_loading_preset
|
||||
(enc->bitrate_manager);
|
||||
res = parent_load_preset (preset, name);
|
||||
gst_encoder_bitrate_profile_manager_end_loading_preset (enc->bitrate_manager,
|
||||
res ? name : NULL);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static void
|
||||
gst_x264_enc_preset_interface_init (GstPresetInterface * iface)
|
||||
{
|
||||
parent_load_preset = iface->load_preset;
|
||||
iface->load_preset = gst_x264_enc_load_preset;
|
||||
}
|
||||
|
||||
#define gst_x264_enc_parent_class parent_class
|
||||
G_DEFINE_TYPE_WITH_CODE (GstX264Enc, gst_x264_enc, GST_TYPE_VIDEO_ENCODER,
|
||||
G_IMPLEMENT_INTERFACE (GST_TYPE_PRESET, NULL));
|
||||
G_IMPLEMENT_INTERFACE (GST_TYPE_PRESET,
|
||||
gst_x264_enc_preset_interface_init));
|
||||
|
||||
/* don't forget to free the string after use */
|
||||
static const gchar *
|
||||
|
@ -1227,7 +1254,6 @@ gst_x264_enc_init (GstX264Enc * encoder)
|
|||
encoder->quantizer = ARG_QUANTIZER_DEFAULT;
|
||||
encoder->mp_cache_file = g_strdup (ARG_MULTIPASS_CACHE_FILE_DEFAULT);
|
||||
encoder->byte_stream = ARG_BYTE_STREAM_DEFAULT;
|
||||
encoder->bitrate = ARG_BITRATE_DEFAULT;
|
||||
encoder->intra_refresh = ARG_INTRA_REFRESH_DEFAULT;
|
||||
encoder->vbv_buf_capacity = ARG_VBV_BUF_CAPACITY_DEFAULT;
|
||||
encoder->me = ARG_ME_DEFAULT;
|
||||
|
@ -1260,6 +1286,9 @@ gst_x264_enc_init (GstX264Enc * encoder)
|
|||
encoder->tune = ARG_TUNE_DEFAULT;
|
||||
encoder->frame_packing = ARG_FRAME_PACKING_DEFAULT;
|
||||
encoder->insert_vui = ARG_INSERT_VUI_DEFAULT;
|
||||
|
||||
encoder->bitrate_manager =
|
||||
gst_encoder_bitrate_profile_manager_new (ARG_BITRATE_DEFAULT);
|
||||
}
|
||||
|
||||
typedef struct
|
||||
|
@ -1384,6 +1413,7 @@ gst_x264_enc_finalize (GObject * object)
|
|||
FREE_STRING (encoder->tunings);
|
||||
FREE_STRING (encoder->option_string);
|
||||
FREE_STRING (encoder->option_string_prop);
|
||||
gst_encoder_bitrate_profile_manager_free (encoder->bitrate_manager);
|
||||
|
||||
#undef FREE_STRING
|
||||
|
||||
|
@ -1497,6 +1527,7 @@ gst_x264_enc_init_encoder (GstX264Enc * encoder)
|
|||
{
|
||||
guint pass = 0;
|
||||
GstVideoInfo *info;
|
||||
guint bitrate;
|
||||
|
||||
if (!encoder->input_state) {
|
||||
GST_DEBUG_OBJECT (encoder, "Have no input state yet");
|
||||
|
@ -1661,6 +1692,10 @@ skip_vui_parameters:
|
|||
|
||||
encoder->x264param.analyse.b_psnr = 0;
|
||||
|
||||
bitrate =
|
||||
gst_encoder_bitrate_profile_manager_get_bitrate (encoder->bitrate_manager,
|
||||
encoder->input_state ? &encoder->input_state->info : NULL);
|
||||
|
||||
/* FIXME 2.0 make configuration more sane and consistent with x264 cmdline:
|
||||
* + split pass property into a pass property (pass1/2/3 enum) and rc-method
|
||||
* + bitrate property should only be used in case of CBR method
|
||||
|
@ -1677,7 +1712,7 @@ skip_vui_parameters:
|
|||
case GST_X264_ENC_PASS_QUAL:
|
||||
encoder->x264param.rc.i_rc_method = X264_RC_CRF;
|
||||
encoder->x264param.rc.f_rf_constant = encoder->quantizer;
|
||||
encoder->x264param.rc.i_vbv_max_bitrate = encoder->bitrate;
|
||||
encoder->x264param.rc.i_vbv_max_bitrate = bitrate;
|
||||
encoder->x264param.rc.i_vbv_buffer_size
|
||||
= encoder->x264param.rc.i_vbv_max_bitrate
|
||||
* encoder->vbv_buf_capacity / 1000;
|
||||
|
@ -1688,8 +1723,8 @@ skip_vui_parameters:
|
|||
case GST_X264_ENC_PASS_PASS3:
|
||||
default:
|
||||
encoder->x264param.rc.i_rc_method = X264_RC_ABR;
|
||||
encoder->x264param.rc.i_bitrate = encoder->bitrate;
|
||||
encoder->x264param.rc.i_vbv_max_bitrate = encoder->bitrate;
|
||||
encoder->x264param.rc.i_bitrate = bitrate;
|
||||
encoder->x264param.rc.i_vbv_max_bitrate = bitrate;
|
||||
encoder->x264param.rc.i_vbv_buffer_size =
|
||||
encoder->x264param.rc.i_vbv_max_bitrate
|
||||
* encoder->vbv_buf_capacity / 1000;
|
||||
|
@ -2031,6 +2066,9 @@ gst_x264_enc_set_src_caps (GstX264Enc * encoder, GstCaps * caps)
|
|||
GstStructure *structure;
|
||||
GstVideoCodecState *state;
|
||||
GstTagList *tags;
|
||||
guint bitrate =
|
||||
gst_encoder_bitrate_profile_manager_get_bitrate (encoder->bitrate_manager,
|
||||
encoder->input_state ? &encoder->input_state->info : NULL);
|
||||
|
||||
outcaps = gst_caps_new_empty_simple ("video/x-h264");
|
||||
structure = gst_caps_get_structure (outcaps, 0);
|
||||
|
@ -2099,8 +2137,8 @@ gst_x264_enc_set_src_caps (GstX264Enc * encoder, GstCaps * caps)
|
|||
tags = gst_tag_list_new_empty ();
|
||||
gst_tag_list_add (tags, GST_TAG_MERGE_REPLACE, GST_TAG_ENCODER, "x264",
|
||||
GST_TAG_ENCODER_VERSION, X264_BUILD,
|
||||
GST_TAG_MAXIMUM_BITRATE, encoder->bitrate * 1024,
|
||||
GST_TAG_NOMINAL_BITRATE, encoder->bitrate * 1024, NULL);
|
||||
GST_TAG_MAXIMUM_BITRATE, bitrate * 1024,
|
||||
GST_TAG_NOMINAL_BITRATE, bitrate * 1024, NULL);
|
||||
gst_video_encoder_merge_tags (GST_VIDEO_ENCODER (encoder), tags,
|
||||
GST_TAG_MERGE_REPLACE);
|
||||
gst_tag_list_unref (tags);
|
||||
|
@ -2545,13 +2583,18 @@ gst_x264_enc_flush_frames (GstX264Enc * encoder, gboolean send)
|
|||
static void
|
||||
gst_x264_enc_reconfig (GstX264Enc * encoder)
|
||||
{
|
||||
guint bitrate;
|
||||
|
||||
if (!encoder->vtable)
|
||||
return;
|
||||
|
||||
bitrate =
|
||||
gst_encoder_bitrate_profile_manager_get_bitrate (encoder->bitrate_manager,
|
||||
encoder->input_state ? &encoder->input_state->info : NULL);
|
||||
switch (encoder->pass) {
|
||||
case GST_X264_ENC_PASS_QUAL:
|
||||
encoder->x264param.rc.f_rf_constant = encoder->quantizer;
|
||||
encoder->x264param.rc.i_vbv_max_bitrate = encoder->bitrate;
|
||||
encoder->x264param.rc.i_vbv_max_bitrate = bitrate;
|
||||
encoder->x264param.rc.i_vbv_buffer_size
|
||||
= encoder->x264param.rc.i_vbv_max_bitrate
|
||||
* encoder->vbv_buf_capacity / 1000;
|
||||
|
@ -2561,8 +2604,8 @@ gst_x264_enc_reconfig (GstX264Enc * encoder)
|
|||
case GST_X264_ENC_PASS_PASS2:
|
||||
case GST_X264_ENC_PASS_PASS3:
|
||||
default:
|
||||
encoder->x264param.rc.i_bitrate = encoder->bitrate;
|
||||
encoder->x264param.rc.i_vbv_max_bitrate = encoder->bitrate;
|
||||
encoder->x264param.rc.i_bitrate = bitrate;
|
||||
encoder->x264param.rc.i_vbv_max_bitrate = bitrate;
|
||||
encoder->x264param.rc.i_vbv_buffer_size
|
||||
= encoder->x264param.rc.i_vbv_max_bitrate
|
||||
* encoder->vbv_buf_capacity / 1000;
|
||||
|
@ -2601,7 +2644,8 @@ gst_x264_enc_set_property (GObject * object, guint prop_id,
|
|||
gst_x264_enc_reconfig (encoder);
|
||||
break;
|
||||
case ARG_BITRATE:
|
||||
encoder->bitrate = g_value_get_uint (value);
|
||||
gst_encoder_bitrate_profile_manager_set_bitrate (encoder->bitrate_manager,
|
||||
g_value_get_uint (value));
|
||||
gst_x264_enc_reconfig (encoder);
|
||||
break;
|
||||
case ARG_VBV_BUF_CAPACITY:
|
||||
|
@ -2820,7 +2864,9 @@ gst_x264_enc_get_property (GObject * object, guint prop_id,
|
|||
g_value_set_boolean (value, encoder->byte_stream);
|
||||
break;
|
||||
case ARG_BITRATE:
|
||||
g_value_set_uint (value, encoder->bitrate);
|
||||
g_value_set_uint (value,
|
||||
gst_encoder_bitrate_profile_manager_get_bitrate
|
||||
(encoder->bitrate_manager, NULL));
|
||||
break;
|
||||
case ARG_INTRA_REFRESH:
|
||||
g_value_set_boolean (value, encoder->intra_refresh);
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
#include <gst/gst.h>
|
||||
#include <gst/video/video.h>
|
||||
#include <gst/video/gstvideoencoder.h>
|
||||
#include "gstencoderbitrateprofilemanager.h"
|
||||
|
||||
#ifdef HAVE_STDINT_H
|
||||
#include <stdint.h>
|
||||
|
@ -126,6 +127,8 @@ struct _GstX264Enc
|
|||
|
||||
/* cached values to set x264_picture_t */
|
||||
gint x264_nplanes;
|
||||
|
||||
GstEncoderBitrateProfileManager *bitrate_manager;
|
||||
};
|
||||
|
||||
struct _GstX264EncClass
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
x264_sources = [
|
||||
'gstx264enc.c',
|
||||
'gstencoderbitrateprofilemanager.c',
|
||||
]
|
||||
|
||||
x264_dep = dependency('x264', required : get_option('x264'),
|
||||
|
|
Loading…
Reference in a new issue