2011-06-11 20:48:32 +02:00
|
|
|
/******************************************************************************\
|
2020-01-01 15:41:43 +01:00
|
|
|
* Copyright (c) 2004-2020
|
2011-06-11 20:48:32 +02:00
|
|
|
*
|
|
|
|
* Author(s):
|
|
|
|
* Volker Fischer
|
|
|
|
*
|
|
|
|
* Note: We are assuming here that put and get operations are secured by a mutex
|
|
|
|
* and accessing does not occur at the same time.
|
|
|
|
*
|
|
|
|
******************************************************************************
|
|
|
|
*
|
|
|
|
* This program is free software; you can redistribute it and/or modify it under
|
|
|
|
* the terms of the GNU General Public License as published by the Free Software
|
|
|
|
* Foundation; either version 2 of the License, or (at your option) any later
|
|
|
|
* version.
|
|
|
|
*
|
|
|
|
* This program 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 General Public License for more
|
|
|
|
* details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License along with
|
|
|
|
* this program; if not, write to the Free Software Foundation, Inc.,
|
2020-06-08 22:58:11 +02:00
|
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
2011-06-11 20:48:32 +02:00
|
|
|
*
|
|
|
|
\******************************************************************************/
|
|
|
|
|
|
|
|
#include "buffer.h"
|
|
|
|
|
|
|
|
|
|
|
|
/* Network buffer implementation **********************************************/
|
|
|
|
void CNetBuf::Init ( const int iNewBlockSize,
|
|
|
|
const int iNewNumBlocks,
|
|
|
|
const bool bPreserve )
|
|
|
|
{
|
|
|
|
// store block size value
|
|
|
|
iBlockSize = iNewBlockSize;
|
|
|
|
|
|
|
|
// total size -> size of one block times the number of blocks
|
2011-06-12 08:15:32 +02:00
|
|
|
CBufferBase<uint8_t>::Init ( iNewBlockSize * iNewNumBlocks,
|
|
|
|
bPreserve );
|
2011-06-11 20:48:32 +02:00
|
|
|
|
2011-06-12 08:15:32 +02:00
|
|
|
// clear buffer if not preserved
|
2011-06-11 20:48:32 +02:00
|
|
|
if ( !bPreserve )
|
|
|
|
{
|
2011-06-12 08:15:32 +02:00
|
|
|
Clear();
|
2011-06-11 20:48:32 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool CNetBuf::Put ( const CVector<uint8_t>& vecbyData,
|
|
|
|
const int iInSize )
|
|
|
|
{
|
|
|
|
bool bPutOK = true;
|
|
|
|
|
2011-06-12 08:15:32 +02:00
|
|
|
// check if there is not enough space available
|
2011-06-11 20:48:32 +02:00
|
|
|
if ( GetAvailSpace() < iInSize )
|
|
|
|
{
|
2011-06-12 08:15:32 +02:00
|
|
|
return false;
|
2011-06-11 20:48:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// copy new data in internal buffer (implemented in base class)
|
|
|
|
CBufferBase<uint8_t>::Put ( vecbyData, iInSize );
|
|
|
|
|
|
|
|
return bPutOK;
|
|
|
|
}
|
|
|
|
|
2014-01-12 10:48:49 +01:00
|
|
|
bool CNetBuf::Get ( CVector<uint8_t>& vecbyData,
|
|
|
|
const int iOutSize )
|
2011-06-11 20:48:32 +02:00
|
|
|
{
|
|
|
|
bool bGetOK = true; // init return value
|
|
|
|
|
|
|
|
// check size
|
2014-01-12 10:48:49 +01:00
|
|
|
if ( ( iOutSize == 0 ) || ( iOutSize != iBlockSize ) )
|
2011-06-11 20:48:32 +02:00
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
2011-06-12 08:15:32 +02:00
|
|
|
|
2013-03-06 17:48:11 +01:00
|
|
|
// check if there is not enough data available
|
2014-01-12 10:48:49 +01:00
|
|
|
if ( GetAvailData() < iOutSize )
|
2011-06-11 20:48:32 +02:00
|
|
|
{
|
2011-06-12 08:15:32 +02:00
|
|
|
return false;
|
2011-06-11 20:48:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// copy data from internal buffer in output buffer (implemented in base
|
|
|
|
// class)
|
2014-01-12 10:48:49 +01:00
|
|
|
CBufferBase<uint8_t>::Get ( vecbyData, iOutSize );
|
2011-06-11 20:48:32 +02:00
|
|
|
|
|
|
|
return bGetOK;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Network buffer with statistic calculations implementation ******************/
|
|
|
|
CNetBufWithStats::CNetBufWithStats() :
|
2020-04-09 21:26:40 +02:00
|
|
|
CNetBuf ( false ), // base class init: no simulation mode
|
|
|
|
iMaxStatisticCount ( MAX_STATISTIC_COUNT ),
|
|
|
|
bUseDoubleSystemFrameSize ( false ),
|
|
|
|
dAutoFilt_WightUpNormal ( IIR_WEIGTH_UP_NORMAL ),
|
|
|
|
dAutoFilt_WightDownNormal ( IIR_WEIGTH_DOWN_NORMAL ),
|
|
|
|
dAutoFilt_WightUpFast ( IIR_WEIGTH_UP_FAST ),
|
2020-04-16 18:40:29 +02:00
|
|
|
dAutoFilt_WightDownFast ( IIR_WEIGTH_DOWN_FAST ),
|
|
|
|
dErrorRateBound ( ERROR_RATE_BOUND ),
|
|
|
|
dUpMaxErrorBound ( UP_MAX_ERROR_BOUND )
|
2011-06-11 20:48:32 +02:00
|
|
|
{
|
2016-01-30 19:22:19 +01:00
|
|
|
// Define the sizes of the simulation buffers,
|
2011-06-11 20:48:32 +02:00
|
|
|
// must be NUM_STAT_SIMULATION_BUFFERS elements!
|
2016-01-30 19:22:19 +01:00
|
|
|
// Avoid the buffer length 1 because we do not have a solution for a
|
|
|
|
// sample rate offset correction. Caused by the jitter we usually get bad
|
|
|
|
// performance with just one buffer.
|
2020-03-28 16:27:45 +01:00
|
|
|
viBufSizesForSim[0] = 2;
|
|
|
|
viBufSizesForSim[1] = 3;
|
|
|
|
viBufSizesForSim[2] = 4;
|
|
|
|
viBufSizesForSim[3] = 5;
|
|
|
|
viBufSizesForSim[4] = 6;
|
|
|
|
viBufSizesForSim[5] = 7;
|
|
|
|
viBufSizesForSim[6] = 8;
|
|
|
|
viBufSizesForSim[7] = 9;
|
|
|
|
viBufSizesForSim[8] = 10;
|
|
|
|
viBufSizesForSim[9] = 11;
|
2011-06-11 20:48:32 +02:00
|
|
|
|
|
|
|
// set all simulation buffers in simulation mode
|
|
|
|
for ( int i = 0; i < NUM_STAT_SIMULATION_BUFFERS; i++ )
|
|
|
|
{
|
|
|
|
SimulationBuffer[i].SetIsSimulation ( true );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-02-23 18:49:44 +01:00
|
|
|
void CNetBufWithStats::GetErrorRates ( CVector<double>& vecErrRates,
|
2015-03-14 17:54:36 +01:00
|
|
|
double& dLimit,
|
|
|
|
double& dMaxUpLimit )
|
2013-02-23 18:49:44 +01:00
|
|
|
{
|
|
|
|
// get all the averages of the error statistic
|
|
|
|
vecErrRates.Init ( NUM_STAT_SIMULATION_BUFFERS );
|
|
|
|
|
|
|
|
for ( int i = 0; i < NUM_STAT_SIMULATION_BUFFERS; i++ )
|
|
|
|
{
|
|
|
|
vecErrRates[i] = ErrorRateStatistic[i].GetAverage();
|
|
|
|
}
|
|
|
|
|
2015-03-14 17:54:36 +01:00
|
|
|
// get the limits for the decisions
|
2020-04-16 18:40:29 +02:00
|
|
|
dLimit = dErrorRateBound;
|
|
|
|
dMaxUpLimit = dUpMaxErrorBound;
|
2013-02-23 18:49:44 +01:00
|
|
|
}
|
|
|
|
|
2011-06-11 20:48:32 +02:00
|
|
|
void CNetBufWithStats::Init ( const int iNewBlockSize,
|
|
|
|
const int iNewNumBlocks,
|
|
|
|
const bool bPreserve )
|
|
|
|
{
|
|
|
|
// call base class Init
|
|
|
|
CNetBuf::Init ( iNewBlockSize, iNewNumBlocks, bPreserve );
|
|
|
|
|
|
|
|
// inits for statistics calculation
|
|
|
|
if ( !bPreserve )
|
|
|
|
{
|
2020-04-09 21:26:40 +02:00
|
|
|
// set the auto filter weights and max statistic count
|
|
|
|
if ( bUseDoubleSystemFrameSize )
|
|
|
|
{
|
|
|
|
dAutoFilt_WightUpNormal = IIR_WEIGTH_UP_NORMAL_DOUBLE_FRAME_SIZE;
|
|
|
|
dAutoFilt_WightDownNormal = IIR_WEIGTH_DOWN_NORMAL_DOUBLE_FRAME_SIZE;
|
|
|
|
dAutoFilt_WightUpFast = IIR_WEIGTH_UP_FAST_DOUBLE_FRAME_SIZE;
|
|
|
|
dAutoFilt_WightDownFast = IIR_WEIGTH_DOWN_FAST_DOUBLE_FRAME_SIZE;
|
|
|
|
iMaxStatisticCount = MAX_STATISTIC_COUNT_DOUBLE_FRAME_SIZE;
|
2020-04-16 18:40:29 +02:00
|
|
|
dErrorRateBound = ERROR_RATE_BOUND_DOUBLE_FRAME_SIZE;
|
|
|
|
dUpMaxErrorBound = UP_MAX_ERROR_BOUND_DOUBLE_FRAME_SIZE;
|
2020-04-09 21:26:40 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
dAutoFilt_WightUpNormal = IIR_WEIGTH_UP_NORMAL;
|
|
|
|
dAutoFilt_WightDownNormal = IIR_WEIGTH_DOWN_NORMAL;
|
|
|
|
dAutoFilt_WightUpFast = IIR_WEIGTH_UP_FAST;
|
|
|
|
dAutoFilt_WightDownFast = IIR_WEIGTH_DOWN_FAST;
|
|
|
|
iMaxStatisticCount = MAX_STATISTIC_COUNT;
|
2020-04-16 18:40:29 +02:00
|
|
|
dErrorRateBound = ERROR_RATE_BOUND;
|
|
|
|
dUpMaxErrorBound = UP_MAX_ERROR_BOUND;
|
2020-04-09 21:26:40 +02:00
|
|
|
}
|
|
|
|
|
2011-06-11 20:48:32 +02:00
|
|
|
for ( int i = 0; i < NUM_STAT_SIMULATION_BUFFERS; i++ )
|
|
|
|
{
|
|
|
|
// init simulation buffers with the correct size
|
|
|
|
SimulationBuffer[i].Init ( iNewBlockSize, viBufSizesForSim[i] );
|
|
|
|
|
|
|
|
// init statistics
|
2020-04-09 21:26:40 +02:00
|
|
|
ErrorRateStatistic[i].Init ( iMaxStatisticCount, true );
|
2011-06-11 20:48:32 +02:00
|
|
|
}
|
2011-06-16 13:35:37 +02:00
|
|
|
|
2015-03-21 12:48:27 +01:00
|
|
|
// reset the initialization counter which controls the initialization
|
|
|
|
// phase length
|
|
|
|
ResetInitCounter();
|
2011-06-16 13:35:37 +02:00
|
|
|
|
2011-06-29 22:20:22 +02:00
|
|
|
// init auto buffer setting with a meaningful value, also init the
|
|
|
|
// IIR parameter with this value
|
2011-06-30 21:51:15 +02:00
|
|
|
iCurAutoBufferSizeSetting = 6;
|
2011-06-29 22:20:22 +02:00
|
|
|
dCurIIRFilterResult = iCurAutoBufferSizeSetting;
|
|
|
|
iCurDecidedResult = iCurAutoBufferSizeSetting;
|
2011-06-11 20:48:32 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-03-21 12:48:27 +01:00
|
|
|
void CNetBufWithStats::ResetInitCounter()
|
|
|
|
{
|
|
|
|
// start initialization phase of IIR filtering, use a quarter the size
|
|
|
|
// of the error rate statistic buffers which should be ok for a good
|
|
|
|
// initialization value (initialization phase should be as short as
|
|
|
|
// possible)
|
2020-04-09 21:26:40 +02:00
|
|
|
iInitCounter = iMaxStatisticCount / 4;
|
2015-03-21 12:48:27 +01:00
|
|
|
}
|
|
|
|
|
2011-06-11 20:48:32 +02:00
|
|
|
bool CNetBufWithStats::Put ( const CVector<uint8_t>& vecbyData,
|
|
|
|
const int iInSize )
|
|
|
|
{
|
|
|
|
// call base class Put
|
|
|
|
const bool bPutOK = CNetBuf::Put ( vecbyData, iInSize );
|
|
|
|
|
|
|
|
// update statistics calculations
|
|
|
|
for ( int i = 0; i < NUM_STAT_SIMULATION_BUFFERS; i++ )
|
|
|
|
{
|
|
|
|
ErrorRateStatistic[i].Update (
|
|
|
|
!SimulationBuffer[i].Put ( vecbyData, iInSize ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
return bPutOK;
|
|
|
|
}
|
|
|
|
|
2014-01-12 10:48:49 +01:00
|
|
|
bool CNetBufWithStats::Get ( CVector<uint8_t>& vecbyData,
|
|
|
|
const int iOutSize )
|
2011-06-11 20:48:32 +02:00
|
|
|
{
|
|
|
|
// call base class Get
|
2014-01-12 10:48:49 +01:00
|
|
|
const bool bGetOK = CNetBuf::Get ( vecbyData, iOutSize );
|
2011-06-11 20:48:32 +02:00
|
|
|
|
|
|
|
// update statistics calculations
|
|
|
|
for ( int i = 0; i < NUM_STAT_SIMULATION_BUFFERS; i++ )
|
|
|
|
{
|
|
|
|
ErrorRateStatistic[i].Update (
|
2014-01-12 10:48:49 +01:00
|
|
|
!SimulationBuffer[i].Get ( vecbyData, iOutSize ) );
|
2011-06-11 20:48:32 +02:00
|
|
|
}
|
|
|
|
|
2011-06-16 13:35:37 +02:00
|
|
|
// update auto setting
|
|
|
|
UpdateAutoSetting();
|
|
|
|
|
2011-06-11 20:48:32 +02:00
|
|
|
return bGetOK;
|
|
|
|
}
|
|
|
|
|
2011-06-16 13:35:37 +02:00
|
|
|
void CNetBufWithStats::UpdateAutoSetting()
|
2011-06-11 20:48:32 +02:00
|
|
|
{
|
2015-03-14 17:54:36 +01:00
|
|
|
int iCurDecision = 0; // dummy initialization
|
|
|
|
int iCurMaxUpDecision = 0; // dummy initialization
|
|
|
|
bool bDecisionFound;
|
2011-06-16 13:35:37 +02:00
|
|
|
|
|
|
|
|
2015-03-14 17:54:36 +01:00
|
|
|
// Get regular error rate decision -----------------------------------------
|
2011-06-11 20:48:32 +02:00
|
|
|
// Use a specified error bound to identify the best buffer size for the
|
|
|
|
// current network situation. Start with the smallest buffer and
|
|
|
|
// test for the error rate until the rate is below the bound.
|
2015-03-14 17:54:36 +01:00
|
|
|
bDecisionFound = false;
|
|
|
|
|
2011-06-11 20:48:32 +02:00
|
|
|
for ( int i = 0; i < NUM_STAT_SIMULATION_BUFFERS - 1; i++ )
|
|
|
|
{
|
2011-06-16 13:35:37 +02:00
|
|
|
if ( ( !bDecisionFound ) &&
|
2020-04-16 18:40:29 +02:00
|
|
|
( ErrorRateStatistic[i].GetAverage() <= dErrorRateBound ) )
|
2011-06-16 13:35:37 +02:00
|
|
|
{
|
|
|
|
iCurDecision = viBufSizesForSim[i];
|
|
|
|
bDecisionFound = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( !bDecisionFound )
|
|
|
|
{
|
|
|
|
// in case no buffer is below bound, use largest buffer size
|
|
|
|
iCurDecision = viBufSizesForSim[NUM_STAT_SIMULATION_BUFFERS - 1];
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-03-14 17:54:36 +01:00
|
|
|
// Get maximum upper error rate decision -----------------------------------
|
|
|
|
// Use a specified error bound to identify the maximum upper error rate
|
|
|
|
// to identify if we have a too low buffer setting which gives a very
|
|
|
|
// bad performance constantly. Start with the smallest buffer and
|
|
|
|
// test for the error rate until the rate is below the bound.
|
|
|
|
bDecisionFound = false;
|
|
|
|
|
|
|
|
for ( int i = 0; i < NUM_STAT_SIMULATION_BUFFERS - 1; i++ )
|
|
|
|
{
|
|
|
|
if ( ( !bDecisionFound ) &&
|
2020-04-16 18:40:29 +02:00
|
|
|
( ErrorRateStatistic[i].GetAverage() <= dUpMaxErrorBound ) )
|
2015-03-14 17:54:36 +01:00
|
|
|
{
|
|
|
|
iCurMaxUpDecision = viBufSizesForSim[i];
|
|
|
|
bDecisionFound = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( !bDecisionFound )
|
|
|
|
{
|
|
|
|
// in case no buffer is below bound, use largest buffer size
|
|
|
|
iCurMaxUpDecision = viBufSizesForSim[NUM_STAT_SIMULATION_BUFFERS - 1];
|
2015-03-21 12:48:27 +01:00
|
|
|
|
|
|
|
// This is a worst case, something very bad had happened. Hopefully
|
|
|
|
// this was just temporary so that we initiate a new initialzation
|
|
|
|
// phase to get quickly back to normal buffer sizes (hopefully).
|
|
|
|
ResetInitCounter();
|
2015-03-14 17:54:36 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2011-06-16 13:35:37 +02:00
|
|
|
// Post calculation (filtering) --------------------------------------------
|
2011-06-29 22:20:22 +02:00
|
|
|
// Define different weigths for up and down direction. Up direction
|
|
|
|
// filtering shall be slower than for down direction since we assume
|
|
|
|
// that the lower value is the actual value which can be used for
|
|
|
|
// the current network condition. If the current error rate estimation
|
|
|
|
// is higher, it may be a temporary problem which should not change
|
|
|
|
// the current jitter buffer size significantly.
|
|
|
|
// For the initialization phase, use lower weight values to get faster
|
|
|
|
// adaptation.
|
2020-04-09 21:26:40 +02:00
|
|
|
double dWeightUp, dWeightDown;
|
2020-03-28 16:27:45 +01:00
|
|
|
const double dHysteresisValue = FILTER_DECISION_HYSTERESIS;
|
2015-03-14 17:54:36 +01:00
|
|
|
bool bUseFastAdaptation = false;
|
2011-06-29 22:20:22 +02:00
|
|
|
|
|
|
|
// check for initialization phase
|
2011-06-16 13:35:37 +02:00
|
|
|
if ( iInitCounter > 0 )
|
|
|
|
{
|
|
|
|
// decrease init counter
|
|
|
|
iInitCounter--;
|
|
|
|
|
2015-03-14 17:54:36 +01:00
|
|
|
// use the fast adaptation
|
|
|
|
bUseFastAdaptation = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// if the current detected buffer setting is below the maximum upper bound
|
|
|
|
// decision, then we enable a booster to go up to the minimum required
|
|
|
|
// number of buffer blocks (i.e. we use weights for fast adaptation)
|
|
|
|
if ( iCurAutoBufferSizeSetting < iCurMaxUpDecision )
|
|
|
|
{
|
|
|
|
bUseFastAdaptation = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( bUseFastAdaptation )
|
|
|
|
{
|
2020-04-09 21:26:40 +02:00
|
|
|
dWeightUp = dAutoFilt_WightUpFast;
|
|
|
|
dWeightDown = dAutoFilt_WightDownFast;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
dWeightUp = dAutoFilt_WightUpNormal;
|
|
|
|
dWeightDown = dAutoFilt_WightDownNormal;
|
2011-06-16 13:35:37 +02:00
|
|
|
}
|
|
|
|
|
2011-06-29 22:20:22 +02:00
|
|
|
// apply non-linear IIR filter
|
2013-03-24 12:38:00 +01:00
|
|
|
MathUtils().UpDownIIR1 ( dCurIIRFilterResult,
|
2013-03-22 19:50:05 +01:00
|
|
|
static_cast<double> ( iCurDecision ),
|
|
|
|
dWeightUp,
|
|
|
|
dWeightDown );
|
2011-06-29 22:20:22 +02:00
|
|
|
|
2015-03-14 17:54:36 +01:00
|
|
|
/*
|
|
|
|
// TEST store important detection parameters in file for debugging
|
|
|
|
static FILE* pFile = fopen ( "test.dat", "w" );
|
|
|
|
static int icnt = 0;
|
|
|
|
if ( icnt == 50 )
|
|
|
|
{
|
|
|
|
fprintf ( pFile, "%d %e\n", iCurDecision, dCurIIRFilterResult );
|
|
|
|
fflush ( pFile );
|
|
|
|
icnt = 0;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
icnt++;
|
|
|
|
}
|
|
|
|
*/
|
|
|
|
|
2011-06-29 22:20:22 +02:00
|
|
|
// apply a hysteresis
|
|
|
|
iCurAutoBufferSizeSetting =
|
2013-03-24 12:38:00 +01:00
|
|
|
MathUtils().DecideWithHysteresis ( dCurIIRFilterResult,
|
2011-06-29 22:20:22 +02:00
|
|
|
iCurDecidedResult,
|
|
|
|
dHysteresisValue );
|
2011-06-30 21:51:15 +02:00
|
|
|
|
|
|
|
|
|
|
|
// Initialization phase check and correction -------------------------------
|
|
|
|
// sometimes in the very first period after a connection we get a bad error
|
|
|
|
// rate result -> delete this from the initialization phase
|
2020-04-09 21:26:40 +02:00
|
|
|
if ( iInitCounter == iMaxStatisticCount / 8 )
|
2011-06-30 21:51:15 +02:00
|
|
|
{
|
|
|
|
// check error rate of the largest buffer as the indicator
|
|
|
|
if ( ErrorRateStatistic[NUM_STAT_SIMULATION_BUFFERS - 1].
|
2020-04-16 18:40:29 +02:00
|
|
|
GetAverage() > dErrorRateBound )
|
2011-06-30 21:51:15 +02:00
|
|
|
{
|
|
|
|
for ( int i = 0; i < NUM_STAT_SIMULATION_BUFFERS; i++ )
|
|
|
|
{
|
|
|
|
ErrorRateStatistic[i].Reset();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2011-06-11 20:48:32 +02:00
|
|
|
}
|