下面列出了怎么用com.google.android.exoplayer2.upstream.DataSpec的API类实例代码及写法,或者点击链接到github查看源代码。
protected Chunk newInitializationChunk(
RepresentationHolder representationHolder,
DataSource dataSource,
Format trackFormat,
int trackSelectionReason,
Object trackSelectionData,
RangedUri initializationUri,
RangedUri indexUri) {
RangedUri requestUri;
String baseUrl = representationHolder.representation.baseUrl;
if (initializationUri != null) {
// It's common for initialization and index data to be stored adjacently. Attempt to merge
// the two requests together to request both at once.
requestUri = initializationUri.attemptMerge(indexUri, baseUrl);
if (requestUri == null) {
requestUri = initializationUri;
}
} else {
requestUri = indexUri;
}
DataSpec dataSpec = new DataSpec(requestUri.resolveUri(baseUrl), requestUri.start,
requestUri.length, representationHolder.representation.getCacheKey());
return new InitializationChunk(dataSource, dataSpec, trackFormat,
trackSelectionReason, trackSelectionData, representationHolder.extractorWrapper);
}
/** Dispatches {@link #onLoadCanceled(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */
public void loadCanceled(
DataSpec dataSpec,
Uri uri,
int dataType,
int trackType,
@Nullable Format trackFormat,
int trackSelectionReason,
@Nullable Object trackSelectionData,
long mediaStartTimeUs,
long mediaEndTimeUs,
long elapsedRealtimeMs,
long loadDurationMs,
long bytesLoaded) {
loadCanceled(
new LoadEventInfo(dataSpec, uri, elapsedRealtimeMs, loadDurationMs, bytesLoaded),
new MediaLoadData(
dataType,
trackType,
trackFormat,
trackSelectionReason,
trackSelectionData,
adjustMediaTime(mediaStartTimeUs),
adjustMediaTime(mediaEndTimeUs)));
}
public SingleSampleMediaPeriod(
DataSpec dataSpec,
DataSource.Factory dataSourceFactory,
@Nullable TransferListener transferListener,
Format format,
long durationUs,
int minLoadableRetryCount,
EventDispatcher eventDispatcher,
boolean treatLoadErrorsAsEndOfStream) {
this.dataSpec = dataSpec;
this.dataSourceFactory = dataSourceFactory;
this.transferListener = transferListener;
this.format = format;
this.durationUs = durationUs;
this.minLoadableRetryCount = minLoadableRetryCount;
this.eventDispatcher = eventDispatcher;
this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream;
tracks = new TrackGroupArray(new TrackGroup(format));
sampleStreams = new ArrayList<>();
loader = new Loader("Loader:SingleSampleMediaPeriod");
eventDispatcher.mediaPeriodCreated();
}
private SingleSampleMediaSource(
Uri uri,
DataSource.Factory dataSourceFactory,
Format format,
long durationUs,
int minLoadableRetryCount,
boolean treatLoadErrorsAsEndOfStream,
@Nullable Object tag) {
this.dataSourceFactory = dataSourceFactory;
this.format = format;
this.durationUs = durationUs;
this.minLoadableRetryCount = minLoadableRetryCount;
this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream;
dataSpec = new DataSpec(uri);
timeline =
new SinglePeriodTimeline(durationUs, /* isSeekable= */ true, /* isDynamic= */ false, tag);
}
@Override
public void onAdLoadError(final AdLoadException error, DataSpec dataSpec) {
if (released) {
return;
}
createEventDispatcher(/* mediaPeriodId= */ null)
.loadError(
dataSpec,
dataSpec.uri,
/* responseHeaders= */ Collections.emptyMap(),
C.DATA_TYPE_AD,
C.TRACK_TYPE_UNKNOWN,
/* loadDurationMs= */ 0,
/* bytesLoaded= */ 0,
error,
/* wasCanceled= */ true);
}
private void addSegment(
HlsMediaPlaylist mediaPlaylist,
HlsMediaPlaylist.Segment segment,
HashSet<Uri> seenEncryptionKeyUris,
ArrayList<Segment> out) {
String baseUri = mediaPlaylist.baseUri;
long startTimeUs = mediaPlaylist.startTimeUs + segment.relativeStartTimeUs;
if (segment.fullSegmentEncryptionKeyUri != null) {
Uri keyUri = UriUtil.resolveToUri(baseUri, segment.fullSegmentEncryptionKeyUri);
if (seenEncryptionKeyUris.add(keyUri)) {
out.add(new Segment(startTimeUs, SegmentDownloader.getCompressibleDataSpec(keyUri)));
}
}
Uri segmentUri = UriUtil.resolveToUri(baseUri, segment.url);
DataSpec dataSpec =
new DataSpec(segmentUri, segment.byterangeOffset, segment.byterangeLength, /* key= */ null);
out.add(new Segment(startTimeUs, dataSpec));
}
/**
* @param dataSource The source from which the data should be loaded.
* @param dataSpec Defines the data to be loaded.
* @param type See {@link #type}.
* @param trackFormat See {@link #trackFormat}.
* @param trackSelectionReason See {@link #trackSelectionReason}.
* @param trackSelectionData See {@link #trackSelectionData}.
* @param startTimeUs See {@link #startTimeUs}.
* @param endTimeUs See {@link #endTimeUs}.
*/
public Chunk(
DataSource dataSource,
DataSpec dataSpec,
int type,
Format trackFormat,
int trackSelectionReason,
@Nullable Object trackSelectionData,
long startTimeUs,
long endTimeUs) {
this.dataSource = new StatsDataSource(dataSource);
this.dataSpec = Assertions.checkNotNull(dataSpec);
this.type = type;
this.trackFormat = trackFormat;
this.trackSelectionReason = trackSelectionReason;
this.trackSelectionData = trackSelectionData;
this.startTimeUs = startTimeUs;
this.endTimeUs = endTimeUs;
}
/** Dispatches {@link #onLoadCanceled(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */
public void loadCanceled(
DataSpec dataSpec,
Uri uri,
int dataType,
long elapsedRealtimeMs,
long loadDurationMs,
long bytesLoaded) {
loadCanceled(
dataSpec,
uri,
dataType,
C.TRACK_TYPE_UNKNOWN,
null,
C.SELECTION_REASON_UNKNOWN,
null,
C.TIME_UNSET,
C.TIME_UNSET,
elapsedRealtimeMs,
loadDurationMs,
bytesLoaded);
}
protected Chunk newInitializationChunk(
RepresentationHolder representationHolder,
DataSource dataSource,
Format trackFormat,
int trackSelectionReason,
Object trackSelectionData,
RangedUri initializationUri,
RangedUri indexUri) {
RangedUri requestUri;
String baseUrl = representationHolder.representation.baseUrl;
if (initializationUri != null) {
// It's common for initialization and index data to be stored adjacently. Attempt to merge
// the two requests together to request both at once.
requestUri = initializationUri.attemptMerge(indexUri, baseUrl);
if (requestUri == null) {
requestUri = initializationUri;
}
} else {
requestUri = indexUri;
}
DataSpec dataSpec = new DataSpec(requestUri.resolveUri(baseUrl), requestUri.start,
requestUri.length, representationHolder.representation.getCacheKey());
return new InitializationChunk(dataSource, dataSpec, trackFormat,
trackSelectionReason, trackSelectionData, representationHolder.extractorWrapper);
}
/**
* Queries the cache to obtain the request length and the number of bytes already cached for a
* given {@link DataSpec}.
*
* @param dataSpec Defines the data to be checked.
* @param cache A {@link Cache} which has the data.
* @param cacheKeyFactory An optional factory for cache keys.
* @return A pair containing the request length and the number of bytes that are already cached.
*/
public static Pair<Long, Long> getCached(
DataSpec dataSpec, Cache cache, @Nullable CacheKeyFactory cacheKeyFactory) {
String key = buildCacheKey(dataSpec, cacheKeyFactory);
long position = dataSpec.absoluteStreamPosition;
long requestLength = getRequestLength(dataSpec, cache, key);
long bytesAlreadyCached = 0;
long bytesLeft = requestLength;
while (bytesLeft != 0) {
long blockLength =
cache.getCachedLength(
key, position, bytesLeft != C.LENGTH_UNSET ? bytesLeft : Long.MAX_VALUE);
if (blockLength > 0) {
bytesAlreadyCached += blockLength;
} else {
blockLength = -blockLength;
if (blockLength == Long.MAX_VALUE) {
break;
}
}
position += blockLength;
bytesLeft -= bytesLeft == C.LENGTH_UNSET ? 0 : blockLength;
}
return Pair.create(requestLength, bytesAlreadyCached);
}
@Override
protected List<Segment> getSegments(
DataSource dataSource, SsManifest manifest, boolean allowIncompleteList) {
ArrayList<Segment> segments = new ArrayList<>();
for (StreamElement streamElement : manifest.streamElements) {
for (int i = 0; i < streamElement.formats.length; i++) {
for (int j = 0; j < streamElement.chunkCount; j++) {
segments.add(
new Segment(
streamElement.getStartTimeUs(j),
new DataSpec(streamElement.buildRequestUri(i, j))));
}
}
}
return segments;
}
/** Dispatches {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */
public void loadStarted(
DataSpec dataSpec,
Uri uri,
int dataType,
int trackType,
@Nullable Format trackFormat,
int trackSelectionReason,
@Nullable Object trackSelectionData,
long mediaStartTimeUs,
long mediaEndTimeUs,
long elapsedRealtimeMs) {
loadStarted(
new LoadEventInfo(
dataSpec, uri, elapsedRealtimeMs, /* loadDurationMs= */ 0, /* bytesLoaded= */ 0),
new MediaLoadData(
dataType,
trackType,
trackFormat,
trackSelectionReason,
trackSelectionData,
adjustMediaTime(mediaStartTimeUs),
adjustMediaTime(mediaEndTimeUs)));
}
private void maybeLoadInitData() throws IOException, InterruptedException {
if (previousExtractor == extractor || initLoadCompleted || initDataSpec == null) {
// According to spec, for packed audio, initDataSpec is expected to be null.
return;
}
DataSpec initSegmentDataSpec = Util.getRemainderDataSpec(initDataSpec, initSegmentBytesLoaded);
try {
ExtractorInput input = new DefaultExtractorInput(initDataSource,
initSegmentDataSpec.absoluteStreamPosition, initDataSource.open(initSegmentDataSpec));
try {
int result = Extractor.RESULT_CONTINUE;
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
result = extractor.read(input, null);
}
} finally {
initSegmentBytesLoaded = (int) (input.getPosition() - initDataSpec.absoluteStreamPosition);
}
} finally {
Util.closeQuietly(dataSource);
}
initLoadCompleted = true;
}
protected Chunk newInitializationChunk(
RepresentationHolder representationHolder,
DataSource dataSource,
Format trackFormat,
int trackSelectionReason,
Object trackSelectionData,
RangedUri initializationUri,
RangedUri indexUri) {
RangedUri requestUri;
String baseUrl = representationHolder.representation.baseUrl;
if (initializationUri != null) {
// It's common for initialization and index data to be stored adjacently. Attempt to merge
// the two requests together to request both at once.
requestUri = initializationUri.attemptMerge(indexUri, baseUrl);
if (requestUri == null) {
requestUri = initializationUri;
}
} else {
requestUri = indexUri;
}
DataSpec dataSpec = new DataSpec(requestUri.resolveUri(baseUrl), requestUri.start,
requestUri.length, representationHolder.representation.getCacheKey());
return new InitializationChunk(dataSource, dataSpec, trackFormat,
trackSelectionReason, trackSelectionData, representationHolder.extractorWrapper);
}
/**
* Prepares exoplayer for audio playback from a local file
* @param uri
*/
private void prepareExoPlayerFromFileUri(Uri uri){
exoPlayer = ExoPlayerFactory.newSimpleInstance(this, new DefaultTrackSelector(null), new DefaultLoadControl());
exoPlayer.addListener(eventListener);
DataSpec dataSpec = new DataSpec(uri);
final FileDataSource fileDataSource = new FileDataSource();
try {
fileDataSource.open(dataSpec);
} catch (FileDataSource.FileDataSourceException e) {
e.printStackTrace();
}
DataSource.Factory factory = new DataSource.Factory() {
@Override
public DataSource createDataSource() {
return fileDataSource;
}
};
MediaSource audioSource = new ExtractorMediaSource(fileDataSource.getUri(),
factory, new DefaultExtractorsFactory(), null, null);
exoPlayer.prepare(audioSource);
initMediaControls();
}
/**
* @param uri Uri of the data to be downloaded.
* @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache
* indexing. May be null.
* @param constructorHelper A {@link DownloaderConstructorHelper} instance.
*/
public ProgressiveDownloader(
Uri uri, @Nullable String customCacheKey, DownloaderConstructorHelper constructorHelper) {
this.dataSpec =
new DataSpec(
uri,
/* absoluteStreamPosition= */ 0,
C.LENGTH_UNSET,
customCacheKey,
/* flags= */ DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION);
this.cache = constructorHelper.getCache();
this.dataSource = constructorHelper.createCacheDataSource();
this.cacheKeyFactory = constructorHelper.getCacheKeyFactory();
this.priorityTaskManager = constructorHelper.getPriorityTaskManager();
isCanceled = new AtomicBoolean();
}
@SuppressWarnings("NonAtomicVolatileUpdate")
@Override
public final void load() throws IOException, InterruptedException {
DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition);
try {
// Create and open the input.
ExtractorInput input = new DefaultExtractorInput(dataSource,
loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec));
if (nextLoadPosition == 0) {
// Configure the output and set it as the target for the extractor wrapper.
BaseMediaChunkOutput output = getOutput();
output.setSampleOffsetUs(sampleOffsetUs);
extractorWrapper.init(
getTrackOutputProvider(output),
clippedStartTimeUs == C.TIME_UNSET
? C.TIME_UNSET
: (clippedStartTimeUs - sampleOffsetUs),
clippedEndTimeUs == C.TIME_UNSET ? C.TIME_UNSET : (clippedEndTimeUs - sampleOffsetUs));
}
// Load and decode the sample data.
try {
Extractor extractor = extractorWrapper.extractor;
int result = Extractor.RESULT_CONTINUE;
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
result = extractor.read(input, DUMMY_POSITION_HOLDER);
}
Assertions.checkState(result != Extractor.RESULT_SEEK);
} finally {
nextLoadPosition = input.getPosition() - dataSpec.absoluteStreamPosition;
}
} finally {
Util.closeQuietly(dataSource);
}
loadCompleted = true;
}
public void loadStarted(final DataSpec dataSpec, final int dataType, final int trackType,
final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData,
final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs) {
if (listener != null) {
handler.post(new Runnable() {
@Override
public void run() {
listener.onLoadStarted(dataSpec, dataType, trackType, trackFormat, trackSelectionReason,
trackSelectionData, adjustMediaTime(mediaStartTimeUs),
adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs);
}
});
}
}
@SuppressWarnings("NonAtomicVolatileUpdate")
@Override
public void load() throws IOException, InterruptedException {
DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition);
try {
// Create and open the input.
long length = dataSource.open(loadDataSpec);
if (length != C.LENGTH_UNSET) {
length += nextLoadPosition;
}
ExtractorInput extractorInput =
new DefaultExtractorInput(dataSource, nextLoadPosition, length);
BaseMediaChunkOutput output = getOutput();
output.setSampleOffsetUs(0);
TrackOutput trackOutput = output.track(0, trackType);
trackOutput.format(sampleFormat);
// Load the sample data.
int result = 0;
while (result != C.RESULT_END_OF_INPUT) {
nextLoadPosition += result;
result = trackOutput.sampleData(extractorInput, Integer.MAX_VALUE, true);
}
int sampleSize = (int) nextLoadPosition;
trackOutput.sampleMetadata(startTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
} finally {
Util.closeQuietly(dataSource);
}
loadCompleted = true;
}
@Override
public long open(DataSpec dataSpec) throws IOException {
Assertions.checkState(!opened);
// DataSpec requires a matching close call even if open fails.
opened = true;
uri = dataSpec.uri;
openedDataSpecs.add(dataSpec);
// If the source knows that the request is unsatisfiable then fail.
if (dataSpec.position >= totalLength || (dataSpec.length != C.LENGTH_UNSET
&& (dataSpec.position + dataSpec.length > totalLength))) {
throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
}
// Scan through the segments, configuring them for the current read.
boolean findingCurrentSegmentIndex = true;
currentSegmentIndex = 0;
int scannedLength = 0;
for (Segment segment : segments) {
segment.bytesRead =
(int) Math.min(Math.max(0, dataSpec.position - scannedLength), segment.length);
scannedLength += segment.length;
findingCurrentSegmentIndex &= segment.isErrorSegment() ? segment.exceptionCleared
: segment.bytesRead == segment.length;
if (findingCurrentSegmentIndex) {
currentSegmentIndex++;
}
}
// Configure bytesRemaining, and return.
if (dataSpec.length == C.LENGTH_UNSET) {
bytesRemaining = totalLength - dataSpec.position;
return simulateUnknownLength ? C.LENGTH_UNSET : bytesRemaining;
} else {
bytesRemaining = dataSpec.length;
return bytesRemaining;
}
}
@Override
public long open(DataSpec dataSpec) throws EncryptedFileDataSourceException {
try {
this.dataSpec = dataSpec;
uri = dataSpec.uri;
File path = new File(dataSpec.uri.getPath());
String name = path.getName();
File keyPath = new File(FileLoader.getInternalCacheDir(), name + ".key");
RandomAccessFile keyFile = new RandomAccessFile(keyPath, "r");
keyFile.read(key);
keyFile.read(iv);
keyFile.close();
file = new RandomAccessFile(path, "r");
file.seek(dataSpec.position);
fileOffset = (int) dataSpec.position;
bytesRemaining = dataSpec.length == C.LENGTH_UNSET ? file.length() - dataSpec.position : dataSpec.length;
if (bytesRemaining < 0) {
throw new EOFException();
}
} catch (IOException e) {
throw new EncryptedFileDataSourceException(e);
}
opened = true;
if (listener != null) {
listener.onTransferStart(this, dataSpec, false);
}
return bytesRemaining;
}
protected static DataSpec getCompressibleDataSpec(Uri uri) {
return new DataSpec(
uri,
/* absoluteStreamPosition= */ 0,
/* length= */ C.LENGTH_UNSET,
/* key= */ null,
/* flags= */ DataSpec.FLAG_ALLOW_GZIP);
}
@Override
public void onLoadError(DataSpec dataSpec, int dataType, int trackType, Format trackFormat,
int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs,
long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded,
IOException error, boolean wasCanceled) {
printInternalError("loadError", error);
}
@Override
public void open(DataSpec dataSpec) throws IOException {
wrappedDataSink.open(dataSpec);
long nonce = CryptoUtil.getFNV64Hash(dataSpec.key);
cipher = new AesFlushingCipher(Cipher.ENCRYPT_MODE, secretKey, nonce,
dataSpec.absoluteStreamPosition);
}
@SuppressWarnings("NonAtomicVolatileUpdate")
@Override
public void load() throws IOException, InterruptedException {
DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition);
try {
// Create and open the input.
ExtractorInput input = new DefaultExtractorInput(dataSource,
loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec));
if (nextLoadPosition == 0) {
extractorWrapper.init(
/* trackOutputProvider= */ null,
/* startTimeUs= */ C.TIME_UNSET,
/* endTimeUs= */ C.TIME_UNSET);
}
// Load and decode the initialization data.
try {
Extractor extractor = extractorWrapper.extractor;
int result = Extractor.RESULT_CONTINUE;
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
result = extractor.read(input, DUMMY_POSITION_HOLDER);
}
Assertions.checkState(result != Extractor.RESULT_SEEK);
} finally {
nextLoadPosition = input.getPosition() - dataSpec.absoluteStreamPosition;
}
} finally {
Util.closeQuietly(dataSource);
}
}
/** Dispatches {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */
public void loadStarted(
DataSpec dataSpec,
int dataType,
int trackType,
@Nullable Format trackFormat,
int trackSelectionReason,
@Nullable Object trackSelectionData,
long mediaStartTimeUs,
long mediaEndTimeUs,
long elapsedRealtimeMs) {
loadStarted(
new LoadEventInfo(
dataSpec,
dataSpec.uri,
/* responseHeaders= */ Collections.emptyMap(),
elapsedRealtimeMs,
/* loadDurationMs= */ 0,
/* bytesLoaded= */ 0),
new MediaLoadData(
dataType,
trackType,
trackFormat,
trackSelectionReason,
trackSelectionData,
adjustMediaTime(mediaStartTimeUs),
adjustMediaTime(mediaEndTimeUs)));
}
/**
* Loads a DASH manifest.
*
* @param dataSource The {@link HttpDataSource} from which the manifest should be read.
* @param manifestUriString The URI of the manifest to be read.
* @return An instance of {@link DashManifest}.
* @throws IOException If an error occurs reading data from the stream.
* @see DashManifestParser
*/
public static DashManifest loadManifest(DataSource dataSource, String manifestUriString)
throws IOException {
DataSourceInputStream inputStream = new DataSourceInputStream(dataSource,
new DataSpec(Uri.parse(manifestUriString), DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH));
try {
inputStream.open();
DashManifestParser parser = new DashManifestParser();
return parser.parse(dataSource.getUri(), inputStream);
} finally {
inputStream.close();
}
}
@Override
public final void onTransferEnd(DataSource dataSource,
DataSpec dataSpec,
boolean isNetwork) {
this.transferListener.onTransferEnd(
dataSource,
dataSpec,
isNetwork
);
}
@SuppressWarnings("NonAtomicVolatileUpdate")
@Override
public final void load() throws IOException, InterruptedException {
DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded);
try {
// Create and open the input.
ExtractorInput input = new DefaultExtractorInput(dataSource,
loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec));
if (bytesLoaded == 0) {
// Configure the output and set it as the target for the extractor wrapper.
BaseMediaChunkOutput output = getOutput();
output.setSampleOffsetUs(sampleOffsetUs);
extractorWrapper.init(output);
}
// Load and decode the sample data.
try {
Extractor extractor = extractorWrapper.extractor;
int result = Extractor.RESULT_CONTINUE;
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
result = extractor.read(input, null);
}
Assertions.checkState(result != Extractor.RESULT_SEEK);
} finally {
bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition);
}
} finally {
Util.closeQuietly(dataSource);
}
loadCompleted = true;
}
private DataSpec buildDataSpec(long position) {
// Disable caching if the content length cannot be resolved, since this is indicative of a
// progressive live stream.
return new DataSpec(
uri,
position,
C.LENGTH_UNSET,
customCacheKey,
DataSpec.FLAG_ALLOW_ICY_METADATA
| DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN
| DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION);
}