mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 19:03:46 +02:00
Dedent
This commit is contained in:
+102
-106
@@ -2651,122 +2651,118 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
||||
): Promise<EncodedPacket | null> {
|
||||
const demuxer = this.internalTrack.demuxer;
|
||||
|
||||
try {
|
||||
let currentFragment: Fragment | null = null;
|
||||
let bestFragment: Fragment | null = null;
|
||||
let bestSampleIndex = -1;
|
||||
let currentFragment: Fragment | null = null;
|
||||
let bestFragment: Fragment | null = null;
|
||||
let bestSampleIndex = -1;
|
||||
|
||||
if (startFragment) {
|
||||
const { sampleIndex, correctSampleFound } = getMatchInFragment(startFragment);
|
||||
if (startFragment) {
|
||||
const { sampleIndex, correctSampleFound } = getMatchInFragment(startFragment);
|
||||
|
||||
if (correctSampleFound) {
|
||||
return this.fetchPacketInFragment(startFragment, sampleIndex, options);
|
||||
if (correctSampleFound) {
|
||||
return this.fetchPacketInFragment(startFragment, sampleIndex, options);
|
||||
}
|
||||
|
||||
if (sampleIndex !== -1) {
|
||||
bestFragment = startFragment;
|
||||
bestSampleIndex = sampleIndex;
|
||||
}
|
||||
}
|
||||
|
||||
// Search for a lookup entry; this way, we won't need to start searching from the start of the file
|
||||
// but can jump right into the correct fragment (or at least nearby).
|
||||
const lookupEntryIndex = binarySearchLessOrEqual(
|
||||
this.internalTrack.fragmentLookupTable,
|
||||
searchTimestamp,
|
||||
x => x.timestamp,
|
||||
);
|
||||
const lookupEntry = lookupEntryIndex !== -1
|
||||
? this.internalTrack.fragmentLookupTable[lookupEntryIndex]!
|
||||
: null;
|
||||
|
||||
const positionCacheIndex = binarySearchLessOrEqual(
|
||||
this.internalTrack.fragmentPositionCache,
|
||||
searchTimestamp,
|
||||
x => x.startTimestamp,
|
||||
);
|
||||
const positionCacheEntry = positionCacheIndex !== -1
|
||||
? this.internalTrack.fragmentPositionCache[positionCacheIndex]!
|
||||
: null;
|
||||
|
||||
const lookupEntryPosition = Math.max(
|
||||
lookupEntry?.moofOffset ?? 0,
|
||||
positionCacheEntry?.moofOffset ?? 0,
|
||||
) || null;
|
||||
|
||||
let currentPos: number;
|
||||
|
||||
if (!startFragment) {
|
||||
currentPos = lookupEntryPosition ?? 0;
|
||||
} else {
|
||||
if (lookupEntryPosition === null || startFragment.moofOffset >= lookupEntryPosition) {
|
||||
currentPos = startFragment.moofOffset + startFragment.moofSize;
|
||||
currentFragment = startFragment;
|
||||
} else {
|
||||
// Use the lookup entry
|
||||
currentPos = lookupEntryPosition;
|
||||
}
|
||||
}
|
||||
|
||||
while (true) {
|
||||
if (currentFragment) {
|
||||
const trackData = currentFragment.trackData.get(this.internalTrack.id);
|
||||
if (trackData && trackData.startTimestamp > latestTimestamp) {
|
||||
// We're already past the upper bound, no need to keep searching
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Load the header
|
||||
let slice = demuxer.reader.requestSliceRange(currentPos, MIN_BOX_HEADER_SIZE, MAX_BOX_HEADER_SIZE);
|
||||
if (slice instanceof Promise) slice = await slice;
|
||||
if (!slice) break;
|
||||
|
||||
const boxStartPos = currentPos;
|
||||
const boxInfo = readBoxHeader(slice);
|
||||
if (!boxInfo) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (boxInfo.name === 'moof') {
|
||||
currentFragment = await demuxer.readFragment(boxStartPos);
|
||||
const { sampleIndex, correctSampleFound } = getMatchInFragment(currentFragment);
|
||||
if (correctSampleFound) {
|
||||
return this.fetchPacketInFragment(currentFragment, sampleIndex, options);
|
||||
}
|
||||
if (sampleIndex !== -1) {
|
||||
bestFragment = startFragment;
|
||||
bestFragment = currentFragment;
|
||||
bestSampleIndex = sampleIndex;
|
||||
}
|
||||
}
|
||||
|
||||
// Search for a lookup entry; this way, we won't need to start searching from the start of the file
|
||||
// but can jump right into the correct fragment (or at least nearby).
|
||||
const lookupEntryIndex = binarySearchLessOrEqual(
|
||||
this.internalTrack.fragmentLookupTable,
|
||||
searchTimestamp,
|
||||
x => x.timestamp,
|
||||
);
|
||||
const lookupEntry = lookupEntryIndex !== -1
|
||||
? this.internalTrack.fragmentLookupTable[lookupEntryIndex]!
|
||||
: null;
|
||||
|
||||
const positionCacheIndex = binarySearchLessOrEqual(
|
||||
this.internalTrack.fragmentPositionCache,
|
||||
searchTimestamp,
|
||||
x => x.startTimestamp,
|
||||
);
|
||||
const positionCacheEntry = positionCacheIndex !== -1
|
||||
? this.internalTrack.fragmentPositionCache[positionCacheIndex]!
|
||||
: null;
|
||||
|
||||
const lookupEntryPosition = Math.max(
|
||||
lookupEntry?.moofOffset ?? 0,
|
||||
positionCacheEntry?.moofOffset ?? 0,
|
||||
) || null;
|
||||
|
||||
let currentPos: number;
|
||||
|
||||
if (!startFragment) {
|
||||
currentPos = lookupEntryPosition ?? 0;
|
||||
} else {
|
||||
if (lookupEntryPosition === null || startFragment.moofOffset >= lookupEntryPosition) {
|
||||
currentPos = startFragment.moofOffset + startFragment.moofSize;
|
||||
currentFragment = startFragment;
|
||||
} else {
|
||||
// Use the lookup entry
|
||||
currentPos = lookupEntryPosition;
|
||||
}
|
||||
}
|
||||
|
||||
while (true) {
|
||||
if (currentFragment) {
|
||||
const trackData = currentFragment.trackData.get(this.internalTrack.id);
|
||||
if (trackData && trackData.startTimestamp > latestTimestamp) {
|
||||
// We're already past the upper bound, no need to keep searching
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Load the header
|
||||
let slice = demuxer.reader.requestSliceRange(currentPos, MIN_BOX_HEADER_SIZE, MAX_BOX_HEADER_SIZE);
|
||||
if (slice instanceof Promise) slice = await slice;
|
||||
if (!slice) break;
|
||||
|
||||
const boxStartPos = currentPos;
|
||||
const boxInfo = readBoxHeader(slice);
|
||||
if (!boxInfo) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (boxInfo.name === 'moof') {
|
||||
currentFragment = await demuxer.readFragment(boxStartPos);
|
||||
const { sampleIndex, correctSampleFound } = getMatchInFragment(currentFragment);
|
||||
if (correctSampleFound) {
|
||||
return this.fetchPacketInFragment(currentFragment, sampleIndex, options);
|
||||
}
|
||||
if (sampleIndex !== -1) {
|
||||
bestFragment = currentFragment;
|
||||
bestSampleIndex = sampleIndex;
|
||||
}
|
||||
}
|
||||
|
||||
currentPos = boxStartPos + boxInfo.totalSize;
|
||||
}
|
||||
|
||||
// Catch faulty lookup table entries
|
||||
if (lookupEntry && (!bestFragment || bestFragment.moofOffset < lookupEntry.moofOffset)) {
|
||||
// The lookup table entry lied to us! We found a lookup entry but no fragment there that satisfied
|
||||
// the match. In this case, let's search again but using the lookup entry before that.
|
||||
const previousLookupEntry = this.internalTrack.fragmentLookupTable[lookupEntryIndex - 1];
|
||||
const newSearchTimestamp = previousLookupEntry?.timestamp ?? -Infinity;
|
||||
return this.performFragmentedLookup(
|
||||
null,
|
||||
getMatchInFragment,
|
||||
newSearchTimestamp,
|
||||
latestTimestamp,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
if (bestFragment) {
|
||||
// If we finished looping but didn't find a perfect match, still return the best match we found
|
||||
return this.fetchPacketInFragment(bestFragment, bestSampleIndex, options);
|
||||
}
|
||||
|
||||
return null;
|
||||
} finally {
|
||||
// release();
|
||||
currentPos = boxStartPos + boxInfo.totalSize;
|
||||
}
|
||||
|
||||
// Catch faulty lookup table entries
|
||||
if (lookupEntry && (!bestFragment || bestFragment.moofOffset < lookupEntry.moofOffset)) {
|
||||
// The lookup table entry lied to us! We found a lookup entry but no fragment there that satisfied
|
||||
// the match. In this case, let's search again but using the lookup entry before that.
|
||||
const previousLookupEntry = this.internalTrack.fragmentLookupTable[lookupEntryIndex - 1];
|
||||
const newSearchTimestamp = previousLookupEntry?.timestamp ?? -Infinity;
|
||||
return this.performFragmentedLookup(
|
||||
null,
|
||||
getMatchInFragment,
|
||||
newSearchTimestamp,
|
||||
latestTimestamp,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
if (bestFragment) {
|
||||
// If we finished looping but didn't find a perfect match, still return the best match we found
|
||||
return this.fetchPacketInFragment(bestFragment, bestSampleIndex, options);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+149
-153
@@ -2077,182 +2077,178 @@ abstract class MatroskaTrackBacking implements InputTrackBacking {
|
||||
): Promise<EncodedPacket | null> {
|
||||
const { demuxer, segment } = this.internalTrack;
|
||||
|
||||
try {
|
||||
let currentCluster: Cluster | null = null;
|
||||
let bestCluster: Cluster | null = null;
|
||||
let bestBlockIndex = -1;
|
||||
let currentCluster: Cluster | null = null;
|
||||
let bestCluster: Cluster | null = null;
|
||||
let bestBlockIndex = -1;
|
||||
|
||||
if (startCluster) {
|
||||
const { blockIndex, correctBlockFound } = getMatchInCluster(startCluster);
|
||||
if (startCluster) {
|
||||
const { blockIndex, correctBlockFound } = getMatchInCluster(startCluster);
|
||||
|
||||
if (correctBlockFound) {
|
||||
return this.fetchPacketInCluster(startCluster, blockIndex, options);
|
||||
}
|
||||
|
||||
if (blockIndex !== -1) {
|
||||
bestCluster = startCluster;
|
||||
bestBlockIndex = blockIndex;
|
||||
}
|
||||
}
|
||||
|
||||
// Search for a cue point; this way, we won't need to start searching from the start of the file
|
||||
// but can jump right into the correct cluster (or at least nearby).
|
||||
const cuePointIndex = binarySearchLessOrEqual(
|
||||
this.internalTrack.cuePoints,
|
||||
searchTimestamp,
|
||||
x => x.time,
|
||||
);
|
||||
const cuePoint = cuePointIndex !== -1
|
||||
? this.internalTrack.cuePoints[cuePointIndex]!
|
||||
: null;
|
||||
|
||||
// Also check the position cache
|
||||
const positionCacheIndex = binarySearchLessOrEqual(
|
||||
this.internalTrack.clusterPositionCache,
|
||||
searchTimestamp,
|
||||
x => x.startTimestamp,
|
||||
);
|
||||
const positionCacheEntry = positionCacheIndex !== -1
|
||||
? this.internalTrack.clusterPositionCache[positionCacheIndex]!
|
||||
: null;
|
||||
|
||||
const lookupEntryPosition = Math.max(
|
||||
cuePoint?.clusterPosition ?? 0,
|
||||
positionCacheEntry?.elementStartPos ?? 0,
|
||||
) || null;
|
||||
|
||||
let currentPos: number;
|
||||
|
||||
if (!startCluster) {
|
||||
currentPos = lookupEntryPosition ?? segment.clusterSeekStartPos;
|
||||
} else {
|
||||
if (lookupEntryPosition === null || startCluster.elementStartPos >= lookupEntryPosition) {
|
||||
currentPos = startCluster.elementEndPos;
|
||||
currentCluster = startCluster;
|
||||
} else {
|
||||
// Use the lookup entry
|
||||
currentPos = lookupEntryPosition;
|
||||
}
|
||||
}
|
||||
|
||||
while (segment.elementEndPos === null || currentPos <= segment.elementEndPos - MIN_HEADER_SIZE) {
|
||||
if (currentCluster) {
|
||||
const trackData = currentCluster.trackData.get(this.internalTrack.id);
|
||||
if (trackData && trackData.startTimestamp > latestTimestamp) {
|
||||
// We're already past the upper bound, no need to keep searching
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Load the header
|
||||
let slice = demuxer.reader.requestSliceRange(currentPos, MIN_HEADER_SIZE, MAX_HEADER_SIZE);
|
||||
if (slice instanceof Promise) slice = await slice;
|
||||
if (!slice) break;
|
||||
|
||||
const elementStartPos = currentPos;
|
||||
const elementHeader = readElementHeader(slice);
|
||||
|
||||
if (
|
||||
!elementHeader
|
||||
|| (!LEVEL_1_EBML_IDS.includes(elementHeader.id) && elementHeader.id !== EBMLId.Void)
|
||||
) {
|
||||
// There's an element here that shouldn't be here. Might be garbage. In this case, let's
|
||||
// try and resync to the next valid element.
|
||||
const nextPos = await resync(
|
||||
demuxer.reader,
|
||||
elementStartPos,
|
||||
LEVEL_1_EBML_IDS,
|
||||
Math.min(segment.elementEndPos ?? Infinity, elementStartPos + MAX_RESYNC_LENGTH),
|
||||
);
|
||||
|
||||
if (nextPos) {
|
||||
currentPos = nextPos;
|
||||
continue;
|
||||
} else {
|
||||
break; // Resync failed
|
||||
}
|
||||
}
|
||||
|
||||
const id = elementHeader.id;
|
||||
let size = elementHeader.size;
|
||||
const dataStartPos = slice.filePos;
|
||||
|
||||
if (id === EBMLId.Cluster) {
|
||||
currentCluster = await demuxer.readCluster(elementStartPos, segment);
|
||||
|
||||
const { blockIndex, correctBlockFound } = getMatchInCluster(currentCluster);
|
||||
if (correctBlockFound) {
|
||||
return this.fetchPacketInCluster(startCluster, blockIndex, options);
|
||||
return this.fetchPacketInCluster(currentCluster, blockIndex, options);
|
||||
}
|
||||
|
||||
if (blockIndex !== -1) {
|
||||
bestCluster = startCluster;
|
||||
bestCluster = currentCluster;
|
||||
bestBlockIndex = blockIndex;
|
||||
}
|
||||
}
|
||||
|
||||
// Search for a cue point; this way, we won't need to start searching from the start of the file
|
||||
// but can jump right into the correct cluster (or at least nearby).
|
||||
const cuePointIndex = binarySearchLessOrEqual(
|
||||
this.internalTrack.cuePoints,
|
||||
searchTimestamp,
|
||||
x => x.time,
|
||||
);
|
||||
const cuePoint = cuePointIndex !== -1
|
||||
? this.internalTrack.cuePoints[cuePointIndex]!
|
||||
: null;
|
||||
|
||||
// Also check the position cache
|
||||
const positionCacheIndex = binarySearchLessOrEqual(
|
||||
this.internalTrack.clusterPositionCache,
|
||||
searchTimestamp,
|
||||
x => x.startTimestamp,
|
||||
);
|
||||
const positionCacheEntry = positionCacheIndex !== -1
|
||||
? this.internalTrack.clusterPositionCache[positionCacheIndex]!
|
||||
: null;
|
||||
|
||||
const lookupEntryPosition = Math.max(
|
||||
cuePoint?.clusterPosition ?? 0,
|
||||
positionCacheEntry?.elementStartPos ?? 0,
|
||||
) || null;
|
||||
|
||||
let currentPos: number;
|
||||
|
||||
if (!startCluster) {
|
||||
currentPos = lookupEntryPosition ?? segment.clusterSeekStartPos;
|
||||
} else {
|
||||
if (lookupEntryPosition === null || startCluster.elementStartPos >= lookupEntryPosition) {
|
||||
currentPos = startCluster.elementEndPos;
|
||||
currentCluster = startCluster;
|
||||
} else {
|
||||
// Use the lookup entry
|
||||
currentPos = lookupEntryPosition;
|
||||
}
|
||||
}
|
||||
|
||||
while (segment.elementEndPos === null || currentPos <= segment.elementEndPos - MIN_HEADER_SIZE) {
|
||||
if (currentCluster) {
|
||||
const trackData = currentCluster.trackData.get(this.internalTrack.id);
|
||||
if (trackData && trackData.startTimestamp > latestTimestamp) {
|
||||
// We're already past the upper bound, no need to keep searching
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Load the header
|
||||
let slice = demuxer.reader.requestSliceRange(currentPos, MIN_HEADER_SIZE, MAX_HEADER_SIZE);
|
||||
if (slice instanceof Promise) slice = await slice;
|
||||
if (!slice) break;
|
||||
|
||||
const elementStartPos = currentPos;
|
||||
const elementHeader = readElementHeader(slice);
|
||||
|
||||
if (
|
||||
!elementHeader
|
||||
|| (!LEVEL_1_EBML_IDS.includes(elementHeader.id) && elementHeader.id !== EBMLId.Void)
|
||||
) {
|
||||
// There's an element here that shouldn't be here. Might be garbage. In this case, let's
|
||||
// try and resync to the next valid element.
|
||||
const nextPos = await resync(
|
||||
demuxer.reader,
|
||||
elementStartPos,
|
||||
LEVEL_1_EBML_IDS,
|
||||
Math.min(segment.elementEndPos ?? Infinity, elementStartPos + MAX_RESYNC_LENGTH),
|
||||
);
|
||||
|
||||
if (nextPos) {
|
||||
currentPos = nextPos;
|
||||
continue;
|
||||
} else {
|
||||
break; // Resync failed
|
||||
}
|
||||
}
|
||||
|
||||
const id = elementHeader.id;
|
||||
let size = elementHeader.size;
|
||||
const dataStartPos = slice.filePos;
|
||||
if (size === null) {
|
||||
// Undefined element size (can happen in livestreamed files). In this case, we need to do some
|
||||
// searching to determine the actual size of the element.
|
||||
|
||||
if (id === EBMLId.Cluster) {
|
||||
currentCluster = await demuxer.readCluster(elementStartPos, segment);
|
||||
// The cluster should have already computed its length, we can just copy that result
|
||||
assert(currentCluster);
|
||||
size = currentCluster.elementEndPos - dataStartPos;
|
||||
} else {
|
||||
// Search for the next element at level 0 or 1
|
||||
const nextElementPos = await searchForNextElementId(
|
||||
demuxer.reader,
|
||||
dataStartPos,
|
||||
LEVEL_0_AND_1_EBML_IDS,
|
||||
segment.elementEndPos,
|
||||
);
|
||||
|
||||
const { blockIndex, correctBlockFound } = getMatchInCluster(currentCluster);
|
||||
if (correctBlockFound) {
|
||||
return this.fetchPacketInCluster(currentCluster, blockIndex, options);
|
||||
}
|
||||
|
||||
if (blockIndex !== -1) {
|
||||
bestCluster = currentCluster;
|
||||
bestBlockIndex = blockIndex;
|
||||
}
|
||||
size = nextElementPos.pos - dataStartPos;
|
||||
}
|
||||
|
||||
if (size === null) {
|
||||
// Undefined element size (can happen in livestreamed files). In this case, we need to do some
|
||||
// searching to determine the actual size of the element.
|
||||
const endPos = dataStartPos + size;
|
||||
if (segment.elementEndPos !== null && endPos > segment.elementEndPos - MIN_HEADER_SIZE) {
|
||||
// No more elements fit in this segment
|
||||
break;
|
||||
} else {
|
||||
// Check the next element. If it's a new segment, we know this segment ends here. The new
|
||||
// segment is just ignored, since we're likely in a livestreamed file and thus only care about
|
||||
// the first segment.
|
||||
|
||||
if (id === EBMLId.Cluster) {
|
||||
// The cluster should have already computed its length, we can just copy that result
|
||||
assert(currentCluster);
|
||||
size = currentCluster.elementEndPos - dataStartPos;
|
||||
} else {
|
||||
// Search for the next element at level 0 or 1
|
||||
const nextElementPos = await searchForNextElementId(
|
||||
demuxer.reader,
|
||||
dataStartPos,
|
||||
LEVEL_0_AND_1_EBML_IDS,
|
||||
segment.elementEndPos,
|
||||
);
|
||||
let slice = demuxer.reader.requestSliceRange(endPos, MIN_HEADER_SIZE, MAX_HEADER_SIZE);
|
||||
if (slice instanceof Promise) slice = await slice;
|
||||
if (!slice) break;
|
||||
|
||||
size = nextElementPos.pos - dataStartPos;
|
||||
}
|
||||
|
||||
const endPos = dataStartPos + size;
|
||||
if (segment.elementEndPos !== null && endPos > segment.elementEndPos - MIN_HEADER_SIZE) {
|
||||
// No more elements fit in this segment
|
||||
const elementId = readElementId(slice);
|
||||
if (elementId === EBMLId.Segment) {
|
||||
segment.elementEndPos = endPos;
|
||||
break;
|
||||
} else {
|
||||
// Check the next element. If it's a new segment, we know this segment ends here. The new
|
||||
// segment is just ignored, since we're likely in a livestreamed file and thus only care about
|
||||
// the first segment.
|
||||
|
||||
let slice = demuxer.reader.requestSliceRange(endPos, MIN_HEADER_SIZE, MAX_HEADER_SIZE);
|
||||
if (slice instanceof Promise) slice = await slice;
|
||||
if (!slice) break;
|
||||
|
||||
const elementId = readElementId(slice);
|
||||
if (elementId === EBMLId.Segment) {
|
||||
segment.elementEndPos = endPos;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentPos = dataStartPos + size;
|
||||
}
|
||||
|
||||
// Catch faulty cue points
|
||||
if (cuePoint && (!bestCluster || bestCluster.elementStartPos < cuePoint.clusterPosition)) {
|
||||
// The cue point lied to us! We found a cue point but no cluster there that satisfied the match. In this
|
||||
// case, let's search again but using the cue point before that.
|
||||
const previousCuePoint = this.internalTrack.cuePoints[cuePointIndex - 1];
|
||||
const newSearchTimestamp = previousCuePoint?.time ?? -Infinity;
|
||||
return this.performClusterLookup(null, getMatchInCluster, newSearchTimestamp, latestTimestamp, options);
|
||||
}
|
||||
|
||||
if (bestCluster) {
|
||||
// If we finished looping but didn't find a perfect match, still return the best match we found
|
||||
return this.fetchPacketInCluster(bestCluster, bestBlockIndex, options);
|
||||
}
|
||||
|
||||
return null;
|
||||
} finally {
|
||||
// release();
|
||||
currentPos = dataStartPos + size;
|
||||
}
|
||||
|
||||
// Catch faulty cue points
|
||||
if (cuePoint && (!bestCluster || bestCluster.elementStartPos < cuePoint.clusterPosition)) {
|
||||
// The cue point lied to us! We found a cue point but no cluster there that satisfied the match. In this
|
||||
// case, let's search again but using the cue point before that.
|
||||
const previousCuePoint = this.internalTrack.cuePoints[cuePointIndex - 1];
|
||||
const newSearchTimestamp = previousCuePoint?.time ?? -Infinity;
|
||||
return this.performClusterLookup(null, getMatchInCluster, newSearchTimestamp, latestTimestamp, options);
|
||||
}
|
||||
|
||||
if (bestCluster) {
|
||||
// If we finished looping but didn't find a perfect match, still return the best match we found
|
||||
return this.fetchPacketInCluster(bestCluster, bestBlockIndex, options);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user