already_AddRefed<MediaRawData> WAVTrackDemuxer::GetFileHeader(const MediaByteRange& aRange) { if (!aRange.Length()) { return nullptr; } RefPtr<MediaRawData> fileHeader = new MediaRawData(); fileHeader->mOffset = aRange.mStart; nsAutoPtr<MediaRawDataWriter> headerWriter(fileHeader->CreateWriter()); if (!headerWriter->SetSize(aRange.Length())) { return nullptr; } const uint32_t read = Read(headerWriter->Data(), fileHeader->mOffset, fileHeader->Size()); if (read != aRange.Length()) { return nullptr; } UpdateState(aRange); return fileHeader.forget(); }
void CombineRanges(const MediaByteRange& pending, const MediaByteRange& preload, MediaByteRange& request) { OP_ASSERT(request.IsEmpty()); // If pending and preload overlap, let request be their union, // clamped to pending.start. if (!pending.IsEmpty() && !preload.IsEmpty()) { request = pending; request.IntersectWith(preload); if (!request.IsEmpty()) { // non-empty intersection => overlap request = pending; request.UnionWith(preload); request.IntersectWith(MediaByteRange(pending.start)); OP_ASSERT(!request.IsEmpty()); } } // Otherwise, pick the first non-empty of pending and preload. if (request.IsEmpty()) { if (!pending.IsEmpty()) request = pending; else request = preload; } }
nsRefPtr<MP4Demuxer::InitPromise> MP4Demuxer::Init() { AutoPinned<mp4_demuxer::ResourceStream> stream(mStream); // Check that we have enough data to read the metadata. MediaByteRange br = mp4_demuxer::MP4Metadata::MetadataRange(stream); if (br.IsNull()) { return InitPromise::CreateAndReject(DemuxerFailureReason::WAITING_FOR_DATA, __func__); } if (!mInitData->SetLength(br.Length(), fallible)) { // OOM return InitPromise::CreateAndReject(DemuxerFailureReason::DEMUXER_ERROR, __func__); } size_t size; mStream->ReadAt(br.mStart, mInitData->Elements(), br.Length(), &size); if (size != size_t(br.Length())) { return InitPromise::CreateAndReject(DemuxerFailureReason::DEMUXER_ERROR, __func__); } nsRefPtr<mp4_demuxer::BufferStream> bufferstream = new mp4_demuxer::BufferStream(mInitData); mMetadata = MakeUnique<mp4_demuxer::MP4Metadata>(bufferstream); if (!mMetadata->GetNumberTracks(mozilla::TrackInfo::kAudioTrack) && !mMetadata->GetNumberTracks(mozilla::TrackInfo::kVideoTrack)) { return InitPromise::CreateAndReject(DemuxerFailureReason::DEMUXER_ERROR, __func__); } return InitPromise::CreateAndResolve(NS_OK, __func__); }
void IntersectWithUnavailable(MediaByteRange& range, URL& url) { if (range.IsEmpty()) return; // find the first unavailable range on or after range.start MediaByteRange unavail; BOOL available = FALSE; OpFileLength length = 0; url.GetPartialCoverage(range.start, available, length, TRUE); if (available) { OP_ASSERT(length > 0); unavail.start = range.start + length; length = 0; url.GetPartialCoverage(unavail.start, available, length, TRUE); OP_ASSERT(!available); } else unavail.start = range.start; // length is now the number of unavailable bytes, or 0 if unknown if (length > 0) unavail.SetLength(length); range.IntersectWith(unavail); }
bool MoofParser::HasMetadata() { int64_t length = std::numeric_limits<int64_t>::max(); mSource->Length(&length); nsTArray<MediaByteRange> byteRanges; byteRanges.AppendElement(MediaByteRange(0, length)); nsRefPtr<mp4_demuxer::BlockingStream> stream = new BlockingStream(mSource); MediaByteRange ftyp; MediaByteRange moov; BoxContext context(stream, byteRanges); for (Box box(&context, mOffset); box.IsAvailable(); box = box.Next()) { if (box.IsType("ftyp")) { ftyp = box.Range(); continue; } if (box.IsType("moov")) { moov = box.Range(); break; } } if (!ftyp.Length() || !moov.Length()) { return false; } mInitRange = ftyp.Extents(moov); return true; }
MediaSourceImpl::State MediaSourceImpl::EnsureBufferingInternal() { MediaByteRange request; CalcRequest(request); if (!request.IsEmpty()) { m_clamp_request = FALSE; if (NeedRestart(request)) { StopBuffering(); return StartBuffering(request); } // The request wasn't restarted, so it may need to be clamped // by aborting it once enough data has become available. if (request.IsFinite() && !IsStreaming()) { OpFileLength loading_end = FILE_LENGTH_NONE; m_use_url->GetAttribute(URL::KHTTPRangeEnd, &loading_end); if (loading_end == FILE_LENGTH_NONE || request.end < loading_end) m_clamp_request = TRUE; } } else { // We have all the data we wanted, so stop buffering if possible. switch (m_state) { case NONE: // Already loaded (data: URL or in cache). return IDLE; case IDLE: case FAILED: // not loading break; case STARTED: case HEADERS: case LOADING: case PAUSED: // Only stop a load if it's in fact already complete or if // it's one that we can later resume. However, when using // the streaming cache, continue loading until either the // cache fills up and PauseBuffering() is called or (if // the request fits in cache) IsLoadedURL() is true. if (IsLoadedURL(m_use_url) || (IsResumableURL(m_use_url) && !IsStreaming())) { StopBuffering(); return IDLE; } break; } } return NONE; }
void MP3TrackDemuxer::UpdateState(const MediaByteRange& aRange) { // Prevent overflow. if (mTotalFrameLen + aRange.Length() < mTotalFrameLen) { // These variables have a linear dependency and are only used to derive the // average frame length. mTotalFrameLen /= 2; mNumParsedFrames /= 2; } // Full frame parsed, move offset to its end. mOffset = aRange.mEnd; mTotalFrameLen += aRange.Length(); if (!mSamplesPerFrame) { mSamplesPerFrame = mParser.CurrentFrame().Header().SamplesPerFrame(); mSamplesPerSecond = mParser.CurrentFrame().Header().SampleRate(); mChannels = mParser.CurrentFrame().Header().Channels(); } ++mNumParsedFrames; ++mFrameIndex; MOZ_ASSERT(mFrameIndex > 0); // Prepare the parser for the next frame parsing session. mParser.EndFrameSession(); }
already_AddRefed<mozilla::MediaByteBuffer> MoofParser::Metadata() { MediaByteRange moov; ScanForMetadata(moov); CheckedInt<MediaByteBuffer::size_type> moovLength = moov.Length(); if (!moovLength.isValid() || !moovLength.value()) { // No moov, or cannot be used as array size. return nullptr; } RefPtr<MediaByteBuffer> metadata = new MediaByteBuffer(); if (!metadata->SetLength(moovLength.value(), fallible)) { LOG(Moof, "OOM"); return nullptr; } RefPtr<BlockingStream> stream = new BlockingStream(mSource); size_t read; bool rv = stream->ReadAt(moov.mStart, metadata->Elements(), moovLength.value(), &read); if (!rv || read != moovLength.value()) { return nullptr; } return metadata.forget(); }
bool MoofParser::HasMetadata() { MediaByteRange ftyp; MediaByteRange moov; ScanForMetadata(ftyp, moov); return !!ftyp.Length() && !!moov.Length(); }
already_AddRefed<MediaRawData> MP3TrackDemuxer::GetNextFrame(const MediaByteRange& aRange) { MP3LOG("GetNext() Begin({mStart=%" PRId64 " Length()=%" PRId64 "})", aRange.mStart, aRange.Length()); if (!aRange.Length()) { return nullptr; } RefPtr<MediaRawData> frame = new MediaRawData(); frame->mOffset = aRange.mStart; nsAutoPtr<MediaRawDataWriter> frameWriter(frame->CreateWriter()); if (!frameWriter->SetSize(aRange.Length())) { MP3LOG("GetNext() Exit failed to allocated media buffer"); return nullptr; } const uint32_t read = Read(frameWriter->Data(), frame->mOffset, frame->Size()); if (read != aRange.Length()) { MP3LOG("GetNext() Exit read=%u frame->Size()=%u", read, frame->Size()); return nullptr; } UpdateState(aRange); frame->mTime = Duration(mFrameIndex - 1).ToMicroseconds(); frame->mDuration = Duration(1).ToMicroseconds(); frame->mTimecode = frame->mTime; frame->mKeyframe = true; MOZ_ASSERT(frame->mTime >= 0); MOZ_ASSERT(frame->mDuration > 0); if (mNumParsedFrames == 1) { // First frame parsed, let's read VBR info if available. // TODO: read info that helps with seeking (bug 1163667). ByteReader reader(frame->Data(), frame->Size()); mParser.ParseVBRHeader(&reader); reader.DiscardRemaining(); mFirstFrameOffset = frame->mOffset; } MP3LOGV("GetNext() End mOffset=%" PRIu64 " mNumParsedFrames=%" PRIu64 " mFrameIndex=%" PRId64 " mTotalFrameLen=%" PRIu64 " mSamplesPerFrame=%d mSamplesPerSecond=%d mChannels=%d", mOffset, mNumParsedFrames, mFrameIndex, mTotalFrameLen, mSamplesPerFrame, mSamplesPerSecond, mChannels); return frame.forget(); }
void WAVTrackDemuxer::UpdateState(const MediaByteRange& aRange) { // Full chunk parsed, move offset to its end. mOffset = aRange.mEnd; mTotalChunkLen += aRange.Length(); }
MediaByteRange MP3TrackDemuxer::FindFirstFrame() { static const int MIN_SUCCESSIVE_FRAMES = 4; MediaByteRange candidateFrame = FindNextFrame(); int numSuccFrames = candidateFrame.Length() > 0; MediaByteRange currentFrame = candidateFrame; MP3LOGV("FindFirst() first candidate frame: mOffset=%" PRIu64 " Length()=%" PRIu64, candidateFrame.mStart, candidateFrame.Length()); while (candidateFrame.Length() && numSuccFrames < MIN_SUCCESSIVE_FRAMES) { mParser.EndFrameSession(); mOffset = currentFrame.mEnd; const MediaByteRange prevFrame = currentFrame; // FindNextFrame() here will only return frames consistent with our candidate frame. currentFrame = FindNextFrame(); numSuccFrames += currentFrame.Length() > 0; // Multiple successive false positives, which wouldn't be caught by the consistency // checks alone, can be detected by wrong alignment (non-zero gap between frames). const int64_t frameSeparation = currentFrame.mStart - prevFrame.mEnd; if (!currentFrame.Length() || frameSeparation != 0) { MP3LOGV("FindFirst() not enough successive frames detected, " "rejecting candidate frame: successiveFrames=%d, last Length()=%" PRIu64 ", last frameSeparation=%" PRId64, numSuccFrames, currentFrame.Length(), frameSeparation); mParser.ResetFrameData(); mOffset = candidateFrame.mStart + 1; candidateFrame = FindNextFrame(); numSuccFrames = candidateFrame.Length() > 0; currentFrame = candidateFrame; MP3LOGV("FindFirst() new candidate frame: mOffset=%" PRIu64 " Length()=%" PRIu64, candidateFrame.mStart, candidateFrame.Length()); } } if (numSuccFrames >= MIN_SUCCESSIVE_FRAMES) { MP3LOG("FindFirst() accepting candidate frame: " "successiveFrames=%d", numSuccFrames); } else { MP3LOG("FindFirst() no suitable first frame found"); } return candidateFrame; }
already_AddRefed<MediaRawData> WAVTrackDemuxer::GetNextChunk(const MediaByteRange& aRange) { if (!aRange.Length()) { return nullptr; } RefPtr<MediaRawData> datachunk = new MediaRawData(); datachunk->mOffset = aRange.mStart; nsAutoPtr<MediaRawDataWriter> chunkWriter(datachunk->CreateWriter()); if (!chunkWriter->SetSize(aRange.Length())) { return nullptr; } const uint32_t read = Read(chunkWriter->Data(), datachunk->mOffset, datachunk->Size()); if (read != aRange.Length()) { return nullptr; } UpdateState(aRange); ++mNumParsedChunks; ++mChunkIndex; datachunk->mTime = Duration(mChunkIndex - 1); if (static_cast<uint32_t>(mChunkIndex) * DATA_CHUNK_SIZE < mDataLength) { datachunk->mDuration = Duration(1); } else { uint32_t mBytesRemaining = mDataLength - mChunkIndex * DATA_CHUNK_SIZE; datachunk->mDuration = DurationFromBytes(mBytesRemaining); } datachunk->mTimecode = datachunk->mTime; datachunk->mKeyframe = true; MOZ_ASSERT(!datachunk->mTime.IsNegative()); MOZ_ASSERT(!datachunk->mDuration.IsNegative()); return datachunk.forget(); }
bool MP3TrackDemuxer::SkipNextFrame(const MediaByteRange& aRange) { if (!mNumParsedFrames || !aRange.Length()) { // We can't skip the first frame, since it could contain VBR headers. nsRefPtr<MediaRawData> frame(GetNextFrame(aRange)); return frame; } UpdateState(aRange); return true; }
void ReduceRanges(const List<MediaSourceClient>& clients, MediaByteRange& pending, MediaByteRange& preload) { OP_ASSERT(preload.IsEmpty()); MediaSourceClient* client = clients.First(); if (client) { for ( ; client; client = client->Suc()) { // take the first non-empty pending if (pending.IsEmpty()) pending = client->GetPending(); // preload the union of all ranges preload.UnionWith(client->GetPreload()); } } else { // with no clients, preload the whole resource preload.start = 0; } }
BOOL MediaSourceImpl::NeedRestart(const MediaByteRange& request) { OP_ASSERT(!request.IsEmpty()); // Note: this function assumes that request is not in cache // If not loading we certainly need to start. if (m_state == NONE || m_state == IDLE) return TRUE; // Only restart resumable resources. if (!IsResumableURL(m_use_url)) return FALSE; // Get the currently loading range. MediaByteRange loading; m_use_url->GetAttribute(URL::KHTTPRangeStart, &loading.start); m_use_url->GetAttribute(URL::KHTTPRangeEnd, &loading.end); OP_ASSERT(!loading.IsEmpty()); // When streaming, adjust the loading range to not include what // has already been evicted from the cache. Note: This must not be // done for a request that was just started, as the cache can then // contain data from the previous request which is not relevant. if (m_state >= LOADING && IsStreaming()) { BOOL available = FALSE; OpFileLength length = 0; GetPartialCoverage(loading.start, available, length); if (!available && (!loading.IsFinite() || length < loading.Length())) loading.start += length; } // Restart if request is before currently loading range. if (request.start < loading.start) return TRUE; // Restart if request is after currently loading range. if (loading.IsFinite() && request.start > loading.end) return TRUE; // request is now a subset of loading, check how much we have left // to load until request.start. BOOL available = FALSE; OpFileLength length = 0; GetPartialCoverage(loading.start, available, length); if (!available) length = 0; if (request.start > loading.start + length) { // FIXME: calculate download rate and time taken to reach offset (CORE-27952) OpFileLength remaining = request.start - (loading.start + length); if (remaining > MEDIA_SOURCE_MAX_WAIT) return TRUE; } return FALSE; }
already_AddRefed<mozilla::MediaByteBuffer> MoofParser::Metadata() { MediaByteRange ftyp; MediaByteRange moov; ScanForMetadata(ftyp, moov); CheckedInt<MediaByteBuffer::size_type> ftypLength = ftyp.Length(); CheckedInt<MediaByteBuffer::size_type> moovLength = moov.Length(); if (!ftypLength.isValid() || !moovLength.isValid() || !ftypLength.value() || !moovLength.value()) { // No ftyp or moov, or they cannot be used as array size. return nullptr; } CheckedInt<MediaByteBuffer::size_type> totalLength = ftypLength + moovLength; if (!totalLength.isValid()) { // Addition overflow, or sum cannot be used as array size. return nullptr; } RefPtr<MediaByteBuffer> metadata = new MediaByteBuffer(); if (!metadata->SetLength(totalLength.value(), fallible)) { // OOM return nullptr; } RefPtr<mp4_demuxer::BlockingStream> stream = new BlockingStream(mSource); size_t read; bool rv = stream->ReadAt(ftyp.mStart, metadata->Elements(), ftypLength.value(), &read); if (!rv || read != ftypLength.value()) { return nullptr; } rv = stream->ReadAt(moov.mStart, metadata->Elements() + ftypLength.value(), moovLength.value(), &read); if (!rv || read != moovLength.value()) { return nullptr; } return metadata.forget(); }
already_AddRefed<MediaRawData> MP3TrackDemuxer::GetNextFrame(const MediaByteRange& aRange) { if (!aRange.Length()) { return nullptr; } nsRefPtr<MediaRawData> frame = new MediaRawData(); frame->mOffset = aRange.mStart; nsAutoPtr<MediaRawDataWriter> frameWriter(frame->CreateWriter()); if (!frameWriter->SetSize(aRange.Length())) { return nullptr; } const uint32_t read = Read(frameWriter->mData, frame->mOffset, frame->mSize); if (read != aRange.Length()) { return nullptr; } UpdateState(aRange); frame->mTime = Duration(mFrameIndex - 1).ToMicroseconds(); frame->mDuration = Duration(1).ToMicroseconds(); MOZ_ASSERT(frame->mTime >= 0); MOZ_ASSERT(frame->mDuration > 0); if (mNumParsedFrames == 1) { // First frame parsed, let's read VBR info if available. // TODO: read info that helps with seeking (bug 1163667). mParser.ParseVBRHeader(frame->mData, frame->mData + frame->mSize); mFirstFrameOffset = frame->mOffset; } return frame.forget(); }
bool MP3TrackDemuxer::SkipNextFrame(const MediaByteRange& aRange) { if (!mNumParsedFrames || !aRange.Length()) { // We can't skip the first frame, since it could contain VBR headers. nsRefPtr<MediaRawData> frame(GetNextFrame(aRange)); return frame; } UpdateState(aRange); MP3DEMUXER_LOGV("SkipNext() End mOffset=%" PRIu64 " mNumParsedFrames=%" PRIu64 " mFrameIndex=%" PRId64 " mTotalFrameLen=%" PRIu64 " mSamplesPerFrame=%d mSamplesPerSecond=%d mChannels=%d", mOffset, mNumParsedFrames, mFrameIndex, mTotalFrameLen, mSamplesPerFrame, mSamplesPerSecond, mChannels); return true; }
nsresult DASHRepDecoder::GetByteRangeForSeek(int64_t const aOffset, MediaByteRange& aByteRange) { NS_ASSERTION(NS_IsMainThread(), "Should be on main thread."); // Only check data ranges if they're available and if this decoder is active, // i.e. inactive rep decoders should only load metadata. ReentrantMonitorAutoEnter mon(GetReentrantMonitor()); for (uint32_t i = 0; i < mByteRanges.Length(); i++) { NS_ENSURE_FALSE(mByteRanges[i].IsNull(), NS_ERROR_NOT_INITIALIZED); // Check if |aOffset| lies within the current data range. if (mByteRanges[i].mStart <= aOffset && aOffset <= mByteRanges[i].mEnd) { if (mMainDecoder->IsDecoderAllowedToDownloadSubsegment(this, i)) { mCurrentByteRange = aByteRange = mByteRanges[i]; mSubsegmentIdx = i; // XXX Hack: should be setting subsegment outside this function, but // need to review seeking for multiple switches anyhow. mMainDecoder->SetSubsegmentIndex(this, i); LOG("Getting DATA range [%d] for seek offset [%lld]: " "bytes [%lld] to [%lld]", i, aOffset, aByteRange.mStart, aByteRange.mEnd); return NS_OK; } break; } } // Don't allow metadata downloads once they're loaded and byte ranges have // been populated. bool canDownloadMetadata = mByteRanges.IsEmpty(); if (canDownloadMetadata) { // Check metadata ranges; init range. if (mInitByteRange.mStart <= aOffset && aOffset <= mInitByteRange.mEnd) { mCurrentByteRange = aByteRange = mInitByteRange; mSubsegmentIdx = 0; LOG("Getting INIT range for seek offset [%lld]: bytes [%lld] to " "[%lld]", aOffset, aByteRange.mStart, aByteRange.mEnd); return NS_OK; } // ... index range. if (mIndexByteRange.mStart <= aOffset && aOffset <= mIndexByteRange.mEnd) { mCurrentByteRange = aByteRange = mIndexByteRange; mSubsegmentIdx = 0; LOG("Getting INDEXES range for seek offset [%lld]: bytes [%lld] to " "[%lld]", aOffset, aByteRange.mStart, aByteRange.mEnd); return NS_OK; } } else { LOG1("Metadata should be read; inhibiting further metadata downloads."); } // If no byte range is found by this stage, clear the parameter and return. aByteRange.Clear(); if (mByteRanges.IsEmpty() || !canDownloadMetadata) { // Assume mByteRanges will be populated after metadata is read. LOG("Data ranges not populated [%s]; metadata download restricted [%s]: " "offset[%lld].", (mByteRanges.IsEmpty() ? "yes" : "no"), (canDownloadMetadata ? "no" : "yes"), aOffset); return NS_ERROR_NOT_AVAILABLE; } else { // Cannot seek to an unknown offset. // XXX Revisit this for dynamic MPD profiles if MPD is regularly updated. LOG("Error! Offset [%lld] is in an unknown range!", aOffset); return NS_ERROR_ILLEGAL_VALUE; } }
already_AddRefed<mozilla::MediaByteBuffer> MoofParser::Metadata() { MediaByteRange ftyp; MediaByteRange moov; ScanForMetadata(ftyp, moov); if (!ftyp.Length() || !moov.Length()) { return nullptr; } RefPtr<MediaByteBuffer> metadata = new MediaByteBuffer(); if (!metadata->SetLength(ftyp.Length() + moov.Length(), fallible)) { // OOM return nullptr; } RefPtr<mp4_demuxer::BlockingStream> stream = new BlockingStream(mSource); size_t read; bool rv = stream->ReadAt(ftyp.mStart, metadata->Elements(), ftyp.Length(), &read); if (!rv || read != ftyp.Length()) { return nullptr; } rv = stream->ReadAt(moov.mStart, metadata->Elements() + ftyp.Length(), moov.Length(), &read); if (!rv || read != moov.Length()) { return nullptr; } return metadata.forget(); }
void MediaSourceImpl::CalcRequest(MediaByteRange& request) { if (IsStreaming()) { // When streaming we only consider the pending range (if any). // When there are no clients, request the entire resource. // To support preload together with streaming, care must be // taken to not restart a preload request [0,Inf] as soon as // data has been evicted and the cached range is e.g. // [500,1000499] if there is no pending request in that range. if (!m_clients.Empty()) { MediaByteRange preload; // unused ReduceRanges(m_clients, request, preload); if (!request.IsEmpty()) { request.SetLength(FILE_LENGTH_NONE); IntersectWithUnavailable(request, m_use_url); if (!request.IsEmpty()) request.SetLength(FILE_LENGTH_NONE); } } else { request.start = 0; } // At this point we should have an unbounded range, but it may // be clamped to the resource length below. OP_ASSERT(!request.IsFinite()); } else { // When not streaming (using multiple range disk cache), both // pending and preload requests are taken into account. // // Example 1: Everything should be preloaded, but since there is a // pending read, start buffering from that offset first. Also, // don't refetch the end of the resource. // // resource: <----------------------> // buffered: <--> <---> // pending: <----> // preload: <----------------------> // request: <--------> // // Example 2: The requested preload is already buffered, so the // request is the empty range. // // resource: <----------------------> // buffered: <--------> // pending: empty range // preload: <-----> // request: empty range MediaByteRange pending, preload; ReduceRanges(m_clients, pending, preload); CombineRanges(pending, preload, request); IntersectWithUnavailable(request, m_use_url); } if (!request.IsEmpty()) { // Extra restrictions if resource length is known (this won't // be needed after CORE-30311 is fixed). OpFileLength resource_length = GetTotalBytes(); if (resource_length > 0) { OP_ASSERT(request.start <= resource_length); MediaByteRange resource(0, resource_length - 1); // Clamp request to resource. request.IntersectWith(resource); // Increase size if it is unreasonably small at the end... if (!request.IsEmpty() && request.Length() < MEDIA_SOURCE_MIN_SIZE && resource.Length() >= MEDIA_SOURCE_MIN_SIZE && request.end == resource.end) { // ... but only if nothing in that range is available. MediaByteRange cand_request(resource_length - MEDIA_SOURCE_MIN_SIZE, resource_length - 1); OP_ASSERT(cand_request.Length() == MEDIA_SOURCE_MIN_SIZE); IntersectWithUnavailable(cand_request, m_use_url); if (cand_request.Length() == MEDIA_SOURCE_MIN_SIZE) request = cand_request; } } } OP_NEW_DBG("CalcRequest", "MediaSource"); OP_DBG(("request: ") << request); }