mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
mpeg-dash-stream
This commit is contained in:
@@ -3312,7 +3312,7 @@ $collections = [
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => ['json'],
|
||||
//'filters' => ['json'],
|
||||
],
|
||||
[
|
||||
'$id' => 'status',
|
||||
|
||||
@@ -76,4 +76,10 @@ return [ // List of publicly visible scopes
|
||||
'health.read' => [
|
||||
'description' => 'Access to read your project\'s health status',
|
||||
],
|
||||
'videos.read' => [
|
||||
'description' => 'Access to read your project\'s video content',
|
||||
],
|
||||
'videos.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s video content',
|
||||
]
|
||||
];
|
||||
|
||||
@@ -767,38 +767,44 @@ App::get('/v1/videos/:videoId/streams/:streamId')
|
||||
$response->setContentType('application/x-mpegurl')->send($template->render(false));
|
||||
} else {
|
||||
$adaptations = [];
|
||||
foreach ($renditions as $rendition) {
|
||||
$adaptationId = 0;
|
||||
foreach ($renditions as $rendition) {
|
||||
$metadata = $rendition->getAttribute('metadata');
|
||||
if (!empty($metadata['Period']['AdaptationSet'])) {
|
||||
foreach ($metadata['Period']['AdaptationSet'] as $adaptationId => $set) {
|
||||
$adaption = $set['@attributes'];
|
||||
$adaption['id'] = $adaptationId;
|
||||
//$adaption['baseUrl'] = $baseUrl . '/renditions/' . $rendition->getId() . '/' . 'segments/';
|
||||
$adaption['representation'] = $set['Representation']['@attributes'];
|
||||
$adaption['representation']['SegmentList'] = $set['Representation']['SegmentList']['@attributes'];
|
||||
|
||||
$representationId = 0;
|
||||
$segments = Authorization::skip(fn() => $dbForProject->find('videos_renditions_segments', [
|
||||
new Query('renditionId', Query::TYPE_EQUAL, [$rendition->getId()]),
|
||||
new Query('representationId', Query::TYPE_EQUAL, [$representationId]),
|
||||
$xml = simplexml_load_string($metadata['xml']);
|
||||
$representationId = 0;
|
||||
foreach ($xml->Period->AdaptationSet as $adaptation) {
|
||||
$representation = [];
|
||||
$representation['id'] = $representationId;
|
||||
$attributes = (array)$adaptation->Representation->attributes();
|
||||
$representation['attributes'] = $attributes['@attributes'] ?? [];
|
||||
$attributes = (array)$adaptation->Representation->SegmentList->attributes();
|
||||
$representation['SegmentList']['attributes'] = $attributes['@attributes'] ?? [];
|
||||
$segments = Authorization::skip(fn() => $dbForProject->find('videos_renditions_segments', [
|
||||
new Query('renditionId', Query::TYPE_EQUAL, [$rendition->getId()]),
|
||||
new Query('representationId', Query::TYPE_EQUAL, [$representationId]),
|
||||
], 1000, 0, ['representationId'], ['ASC']));
|
||||
|
||||
foreach ($segments as $segment) {
|
||||
if ($segment->getAttribute('isInit')) {
|
||||
$adaption['representation']['SegmentList']['Initialization'] = $baseUrl . '/renditions/' . $rendition->getId() . '/' . 'segments/' . $segment->getId();
|
||||
continue;
|
||||
}
|
||||
$adaption['representation']['SegmentList']['media'][] = $baseUrl . '/renditions/' . $rendition->getId() . '/' . 'segments/' . $segment->getId();
|
||||
foreach ($segments ?? [] as $segment) {
|
||||
if ($segment->getAttribute('isInit')) {
|
||||
$representation['SegmentList']['Initialization'] = $segment->getId();
|
||||
continue;
|
||||
}
|
||||
$adaptations[] = $adaption;
|
||||
$representationId++;
|
||||
$representation['SegmentList']['media'][] = $segment->getId();
|
||||
}
|
||||
$attributes = (array)$adaptation->attributes();
|
||||
$adaptations[] = ['attributes' => $attributes['@attributes'] ?? [],
|
||||
'id' => $adaptationId,
|
||||
'baseUrl' => $baseUrl . '/renditions/' . $rendition->getId() . '/' . 'segments/',
|
||||
'Representation' => $representation,
|
||||
];
|
||||
$adaptationId++;
|
||||
$representationId++;
|
||||
}
|
||||
|
||||
$template = new View(__DIR__ . '/../../views/videos/dash.phtml');
|
||||
$template->setParam('params', $adaptations);
|
||||
$response->setContentType('application/dash+xml')->send($template->render(false));
|
||||
}
|
||||
|
||||
$template = new View(__DIR__ . '/../../views/videos/dash.phtml');
|
||||
$template->setParam('params', $adaptations);
|
||||
$response->setContentType('application/dash+xml')->send($template->render(false));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -945,9 +951,10 @@ App::get('/v1/videos/:videoId/streams/:streamId/subtitles/:subtitleId')
|
||||
->param('subtitleId', '', new UID(), 'Subtitle unique ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('deviceVideos')
|
||||
->inject('mode')
|
||||
->inject('user')
|
||||
->action(function (string $videoId, string $streamId, string $subtitleId, Response $response, Database $dbForProject, string $mode, Document $user) {
|
||||
->action(function (string $videoId, string $streamId, string $subtitleId, Response $response, Database $dbForProject, Device $deviceVideos, string $mode, Document $user) {
|
||||
|
||||
$video = Authorization::skip(fn() => $dbForProject->findOne('videos', [
|
||||
new Query('_uid', Query::TYPE_EQUAL, [$videoId])
|
||||
@@ -968,26 +975,31 @@ App::get('/v1/videos/:videoId/streams/:streamId/subtitles/:subtitleId')
|
||||
throw new Exception('subtitle not found', 404, Exception::VIDEO_SUBTITLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$segments = Authorization::skip(fn () => $dbForProject->find('videos_subtitles_segments', [
|
||||
new Query('subtitleId', Query::TYPE_EQUAL, [$subtitleId]),
|
||||
], 4000));
|
||||
if ($streamId == 'hls') {
|
||||
$segments = Authorization::skip(fn() => $dbForProject->find('videos_subtitles_segments', [
|
||||
new Query('subtitleId', Query::TYPE_EQUAL, [$subtitleId]),
|
||||
], 4000));
|
||||
|
||||
if (empty($segments)) {
|
||||
throw new Exception('Subtitle segments not found', 404, Exception::VIDEO_SUBTITLE_SEGMENT_NOT_FOUND);
|
||||
if (empty($segments)) {
|
||||
throw new Exception('Subtitle segments not found', 404, Exception::VIDEO_SUBTITLE_SEGMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$paramsSegments = [];
|
||||
foreach ($segments as $segment) {
|
||||
$paramsSegments[] = [
|
||||
'duration' => $segment->getAttribute('duration'),
|
||||
'url' => 'http://127.0.0.1/v1/videos/' . $videoId . '/streams/' . $streamId . '/subtitles/' . $subtitleId . '/segments/' . $segment->getId(),
|
||||
];
|
||||
}
|
||||
|
||||
$template = new View(__DIR__ . '/../../views/videos/hls-subtitles.phtml');
|
||||
$template->setParam('targetDuration', $subtitle->getAttribute('targetDuration'));
|
||||
$template->setParam('paramsSegments', $paramsSegments);
|
||||
$response->setContentType('application/x-mpegurl')->send($template->render(false));
|
||||
} else {
|
||||
$output = $deviceVideos->read($deviceVideos->getPath($subtitle->getAttribute('videoId') . $subtitle->getAttribute('fileId') . '.vtt'));
|
||||
$response->setContentType('text/vtt')->send($output);
|
||||
}
|
||||
|
||||
$paramsSegments = [];
|
||||
foreach ($segments as $segment) {
|
||||
$paramsSegments[] = [
|
||||
'duration' => $segment->getAttribute('duration'),
|
||||
'url' => 'http://127.0.0.1/v1/videos/' . $videoId . '/streams/' . $streamId . '/subtitles/' . $subtitleId . '/segments/' . $segment->getId() ,
|
||||
];
|
||||
}
|
||||
|
||||
$template = new View(__DIR__ . '/../../views/videos/hls-subtitles.phtml');
|
||||
$template->setParam('targetDuration', $subtitle->getAttribute('targetDuration'));
|
||||
$template->setParam('paramsSegments', $paramsSegments);
|
||||
$response->setContentType('application/x-mpegurl')->send($template->render(false));
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -843,6 +843,7 @@ App::setResource('project', function ($dbForConsole, $request, $console) {
|
||||
/** @var Utopia\Database\Database $dbForConsole */
|
||||
/** @var Utopia\Database\Document $console */
|
||||
|
||||
$projectId = 'dev';
|
||||
$projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', 'console'));
|
||||
|
||||
if ($projectId === 'console') {
|
||||
|
||||
+15
-11
@@ -1,25 +1,29 @@
|
||||
<?php echo '<?xml version="1.0" encoding="utf-8"?>'; ?>
|
||||
<?php
|
||||
|
||||
echo '<?xml version="1.0" encoding="utf-8"?>'; ?>
|
||||
<MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:mpeg:dash:schema:mpd:2011" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd"
|
||||
profiles="urn:mpeg:dash:profile:isoff-live:2011" type="static" mediaPresentationDuration="PT13.2S" maxSegmentDuration="PT6.0S" minBufferTime="PT16.6S">
|
||||
<ProgramInformation></ProgramInformation>
|
||||
<ServiceDescription id="0"></ServiceDescription>
|
||||
<Period id="0" start="PT0.0S">
|
||||
<?php foreach ($this->getParam('params', []) as $param): ?>
|
||||
<BaseURL><?php if (isset($param['baseUrl'])): echo $param['baseUrl']; endif;?></BaseURL>
|
||||
<AdaptationSet <?php if (isset($param['id'])):?> id="<?php echo $param['id']; ?>"<?php endif;?><?php if (isset($param['contentType'])): ?> contentType="<?php echo $param['contentType']; ?>"<?php endif;?><?php if (isset($param['startWithSAP'])): ?> startWithSAP="<?php echo $param['startWithSAP']; ?>"<?php endif;?><?php if (isset($param['segmentAlignment'])): ?> segmentAlignment="<?php echo $param['segmentAlignment']; ?>"<?php endif;?><?php if (isset($param['bitstreamSwitching'])): ?> bitstreamSwitching="<?php echo $param['bitstreamSwitching']; ?>"<?php endif;?><?php if (isset($param['frameRate'])): ?> frameRate="<?php echo $param['frameRate']; ?>"<?php endif;?><?php if (isset($param['maxWidth'])): ?> maxWidth="<?php echo $param['maxWidth']; ?>"<?php endif;?><?php if (isset($param['par'])): ?> par="<?php echo $param['par']; ?>"<?php endif;?><?php if (isset($param['lang'])): ?> lang="<?php echo $param['lang']; ?>"<?php endif;?>>
|
||||
<Representation <?php if (isset($param['representation']['id'])):?> id="<?php echo $param['representation']['id']; ?>"<?php endif;?><?php if (isset($param['representation']['mimeType'])):?> mimeType="<?php echo $param['representation']['mimeType']; ?>"<?php endif;?><?php if (isset($param['representation']['codecs'])):?> codecs="<?php echo $param['representation']['codecs']; ?>"<?php endif;?><?php if (isset($param['representation']['width'])):?> width="<?php echo $param['representation']['width']; ?>"<?php endif;?><?php if (isset($param['representation']['height'])):?> height="<?php echo $param['representation']['height']; ?>"<?php endif;?><?php if (isset($param['representation']['sar'])):?> sar="<?php echo $param['representation']['sar']; ?>"<?php endif;?><?php if (isset($param['representation']['audioSamplingRate'])):?> audioSamplingRate="<?php echo $param['representation']['audioSamplingRate']; ?>"<?php endif;?>>
|
||||
<?php if ($param['contentType'] === 'audio'):?>
|
||||
<?php foreach ($this->getParam('params', []) as $param):
|
||||
$representation = $param['Representation'];
|
||||
?>
|
||||
<AdaptationSet <?php if (isset($param['id'])):?> id="<?php echo $param['id']; ?>"<?php endif;?><?php if (isset($param['attributes']['contentType'])): ?> contentType="<?php echo $param['attributes']['contentType']; ?>"<?php endif;?><?php if (isset($param['attributes']['startWithSAP'])): ?> startWithSAP="<?php echo $param['attributes']['startWithSAP']; ?>"<?php endif;?><?php if (isset($param['attributes']['segmentAlignment'])): ?> segmentAlignment="<?php echo $param['attributes']['segmentAlignment']; ?>"<?php endif;?><?php if (isset($param['attributes']['bitstreamSwitching'])): ?> bitstreamSwitching="<?php echo $param['attributes']['bitstreamSwitching']; ?>"<?php endif;?><?php if (isset($param['attributes']['frameRate'])): ?> frameRate="<?php echo $param['attributes']['frameRate']; ?>"<?php endif;?><?php if (isset($param['attributes']['maxWidth'])): ?> maxWidth="<?php echo $param['attributes']['maxWidth']; ?>"<?php endif;?><?php if (isset($param['attributes']['par'])): ?> par="<?php echo $param['attributes']['par']; ?>"<?php endif;?><?php if (isset($param['attributes']['lang'])): ?> lang="<?php echo $param['attributes']['lang']; ?>"<?php endif;?>>
|
||||
<Representation <?php if (isset($representation['attributes']['id'])):?> id="<?php echo $representation['attributes']['id']; ?>"<?php endif;?><?php if (isset($representation['attributes']['mimeType'])):?> mimeType="<?php echo $representation['attributes']['mimeType']; ?>"<?php endif;?><?php if (isset($representation['attributes']['codecs'])):?> codecs="<?php echo $representation['attributes']['codecs']; ?>"<?php endif;?><?php if (isset($representation['attributes']['width'])):?> width="<?php echo $representation['attributes']['width']; ?>"<?php endif;?><?php if (isset($representation['attributes']['height'])):?> height="<?php echo $representation['attributes']['height']; ?>"<?php endif;?><?php if (isset($representation['attributes']['sar'])):?> sar="<?php echo $representation['attributes']['sar']; ?>"<?php endif;?><?php if (isset($representation['attributes']['audioSamplingRate'])):?> audioSamplingRate="<?php echo $representation['attributes']['audioSamplingRate']; ?>"<?php endif;?>>
|
||||
<BaseURL><?php if (isset($param['baseUrl'])): echo $param['baseUrl']; endif;?></BaseURL>
|
||||
<?php if (isset($param['attributes']['contentType']) && $param['attributes']['contentType'] === 'audio' ):?>
|
||||
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2" />
|
||||
<?php endif;?>
|
||||
|
||||
<Initialization sourceURL="<?php if (isset($param['representation']['SegmentList']['Initialization'])):?><?php echo $param['representation']['SegmentList']['Initialization']; ?><?php endif;?>"/>
|
||||
|
||||
<?php if (isset($param['representation']['SegmentList']['media'])):?>
|
||||
<?php foreach ($param['representation']['SegmentList']['media'] as $segment): ?>
|
||||
<SegmentList<?php if (isset($representation['SegmentList']['attributes']['timescale'])): ?> timescale="<?php echo $representation['SegmentList']['attributes']['timescale']; ?>"<?php endif;?><?php if (isset($representation['SegmentList']['attributes']['duration'])): ?> duration="<?php echo $representation['SegmentList']['attributes']['duration']; ?>"<?php endif;?><?php if (isset($representation['SegmentList']['attributes']['startNumber'])): ?> startNumber="<?php echo $representation['SegmentList']['attributes']['startNumber']; ?>"<?php endif;?>>
|
||||
<Initialization sourceURL="<?php if (isset($representation['SegmentList']['Initialization'])):?><?php echo $representation['SegmentList']['Initialization']; ?><?php endif;?>"/>
|
||||
<?php if (isset($representation['SegmentList']['media'])):?>
|
||||
<?php foreach ($representation['SegmentList']['media'] as $segment): ?>
|
||||
<SegmentURL media="<?php if (isset($segment)):?><?php echo $segment; ?><?php endif;?>"/>
|
||||
<?php endforeach; ?>
|
||||
<?php endif;?>
|
||||
</SegmentList>
|
||||
</Representation>
|
||||
</AdaptationSet>
|
||||
<?php endforeach; ?>
|
||||
|
||||
+18
-19
@@ -132,7 +132,7 @@ class TranscodingV1 extends Worker
|
||||
}
|
||||
}
|
||||
|
||||
$general = $this->getVideoSourceInfo($ffprobe->streams($inPath));
|
||||
$general = $this->getVideoSourceInfo($ffprobe->streams($inPath));
|
||||
if (!empty($general)) {
|
||||
foreach ($general as $key => $value) {
|
||||
$sourceVideo->setAttribute($key, $value);
|
||||
@@ -255,7 +255,6 @@ class TranscodingV1 extends Worker
|
||||
}
|
||||
}
|
||||
$query->setAttribute('targetDuration', $m3u8['targetDuration']);
|
||||
|
||||
} else {
|
||||
|
||||
$mpd = $this->parseMpd($this->outPath . '.mpd');
|
||||
@@ -272,8 +271,9 @@ class TranscodingV1 extends Worker
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($mpd['metadata'])) {
|
||||
$query->setAttribute('metadata', json_encode($mpd['metadata']));
|
||||
$query->setAttribute('metadata', json_encode(['xml' => $mpd['metadata']]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,12 +411,12 @@ class TranscodingV1 extends Worker
|
||||
{
|
||||
$segments = [];
|
||||
$metadata = null;
|
||||
$representationId = 0;
|
||||
$handle = fopen($path, "r");
|
||||
if ($handle) {
|
||||
$representationId = -1;
|
||||
while (($line = fgets($handle)) !== false) {
|
||||
$line = str_replace([",","\r","\n"], "", $line);
|
||||
if (str_contains($line, "contentType=\"audio\"")) {
|
||||
if (str_contains($line, "<AdaptationSet")) {
|
||||
$representationId++;
|
||||
}
|
||||
|
||||
@@ -426,17 +426,15 @@ class TranscodingV1 extends Worker
|
||||
$segments[] = [
|
||||
'isInit' => str_contains($line, "Initialization") ? 1 : 0,
|
||||
'representationId' => $representationId,
|
||||
'fileName' => str_replace(["<SegmentURL media=\"","<Initialization sourceURL=\"", "\"/>", " "], "", $line),
|
||||
'fileName' => trim(str_replace(["<SegmentURL media=\"", "<Initialization sourceURL=\"", "\"/>", "\" />"], "", $line)),
|
||||
];
|
||||
}
|
||||
}
|
||||
fclose($handle);
|
||||
}
|
||||
|
||||
$xml = simplexml_load_string($metadata);
|
||||
|
||||
return [
|
||||
'metadata' => json_decode(json_encode((array)$xml), true),
|
||||
'metadata' => $metadata,
|
||||
'segments' => $segments
|
||||
];
|
||||
}
|
||||
@@ -483,17 +481,18 @@ class TranscodingV1 extends Worker
|
||||
*/
|
||||
private function getVideoSourceInfo(StreamCollection $streams): array
|
||||
{
|
||||
|
||||
return [
|
||||
'duration' => !empty($streams->videos()) ? $streams->videos()->first()->get('duration') : '0',
|
||||
'height' => !empty($streams->videos()) ? $streams->videos()->first()->get('height') : 0,
|
||||
'width' => !empty($streams->videos()) ? $streams->videos()->first()->get('width') : 0,
|
||||
'videoCodec' => !empty($streams->videos()) ? $streams->videos()->first()->get('codec_name') . ',' . $streams->videos()->first()->get('codec_tag_string') : '',
|
||||
'videoFramerate' => !empty($streams->videos()) ? $streams->videos()->first()->get('avg_frame_rate') : '',
|
||||
'videoBitrate' => !empty($streams->videos()) ? (int)$streams->videos()->first()->get('bit_rate') : 0,
|
||||
'audioCodec' => !empty($streams->audios()) ? $streams->audios()->first()->get('codec_name') . ',' . $streams->audios()->first()->get('codec_tag_string') : '',
|
||||
'audioSamplerate' => !empty($streams->audios()) ? (int)$streams->audios()->first()->get('sample_rate') : 0,
|
||||
'audioBitrate' => !empty($streams->audios()) ? (int)$streams->audios()->first()->get('bit_rate') : 0,
|
||||
];
|
||||
'duration' => $streams->videos()->count() ? $streams->videos()->first()->get('duration') : '0',
|
||||
'height' => $streams->videos()->count() ? $streams->videos()->first()->get('height') : 0,
|
||||
'width' => $streams->videos()->count() ? $streams->videos()->first()->get('width') : 0,
|
||||
'videoCodec' => $streams->videos()->count() ? $streams->videos()->first()->get('codec_name') . ',' . $streams->videos()->first()->get('codec_tag_string') : '',
|
||||
'videoFramerate' => $streams->videos()->count() ? $streams->videos()->first()->get('avg_frame_rate') : '',
|
||||
'videoBitrate' => $streams->videos()->count() > 0 ? (int)$streams->videos()->first()->get('bit_rate') : 0,
|
||||
'audioCodec' => $streams->audios()->count() > 0 ? $streams->audios()->first()->get('codec_name') . ',' . $streams->audios()->first()->get('codec_tag_string') : '',
|
||||
'audioSamplerate' => $streams->audios()->count() > 0 ? (int)$streams->audios()->first()->get('sample_rate') : 0,
|
||||
'audioBitrate' => $streams->audios()->count() > 0 ? (int)$streams->audios()->first()->get('bit_rate') : 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@
|
||||
"utopia-php/preloader": "0.2.*",
|
||||
"utopia-php/domains": "1.1.*",
|
||||
"utopia-php/swoole": "0.3.*",
|
||||
"utopia-php/storage": "dev-main",
|
||||
"utopia-php/storage": "dev-feat-transfer",
|
||||
"utopia-php/websocket": "0.1.0",
|
||||
"utopia-php/image": "0.5.*",
|
||||
"utopia-php/orchestration": "0.6.*",
|
||||
|
||||
Generated
-6322
File diff suppressed because it is too large
Load Diff
@@ -409,7 +409,7 @@ class VideoCustomServerTest extends Scope
|
||||
|
||||
sleep(20);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/videos/' . $data['videoId'] . '/streams/mpeg-dash', [
|
||||
$response = $this->client->call(Client::METHOD_GET, '/videos/' . $data['videoId'] . '/streams/dash', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
@@ -418,17 +418,5 @@ class VideoCustomServerTest extends Scope
|
||||
var_dump($response['body']);
|
||||
}
|
||||
|
||||
public function testBla(){
|
||||
$response = $this->client->call(Client::METHOD_GET, '/videos/62dfaeb69c97ea81c58c/streams/dash/renditions/62dfaeb83bc30e827719/segments/62dfaed714324026902c', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]);
|
||||
|
||||
var_dump($response['body']);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user