michael@0: /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim:set ts=2 sw=2 sts=2 et cindent: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "mozilla/DebugOnly.h" michael@0: michael@0: #include "nsError.h" michael@0: #include "MediaDecoderStateMachine.h" michael@0: #include "MediaDecoder.h" michael@0: #include "OggReader.h" michael@0: #include "VideoUtils.h" michael@0: #include "theora/theoradec.h" michael@0: #include michael@0: #ifdef MOZ_OPUS michael@0: #include "opus/opus.h" michael@0: extern "C" { michael@0: #include "opus/opus_multistream.h" michael@0: } michael@0: #endif michael@0: #include "mozilla/dom/TimeRanges.h" michael@0: #include "mozilla/TimeStamp.h" michael@0: #include "VorbisUtils.h" michael@0: #include "MediaMetadataManager.h" michael@0: #include "nsISeekableStream.h" michael@0: #include "gfx2DGlue.h" michael@0: michael@0: using namespace mozilla::gfx; michael@0: michael@0: namespace mozilla { michael@0: michael@0: // On B2G estimate the buffered ranges rather than calculating them explicitly. michael@0: // This prevents us doing I/O on the main thread, which is prohibited in B2G. michael@0: #ifdef MOZ_WIDGET_GONK michael@0: #define OGG_ESTIMATE_BUFFERED 1 michael@0: #endif michael@0: michael@0: // Un-comment to enable logging of seek bisections. michael@0: //#define SEEK_LOGGING michael@0: michael@0: #ifdef PR_LOGGING michael@0: extern PRLogModuleInfo* gMediaDecoderLog; michael@0: #define LOG(type, msg) PR_LOG(gMediaDecoderLog, type, msg) michael@0: #ifdef SEEK_LOGGING michael@0: #define SEEK_LOG(type, msg) PR_LOG(gMediaDecoderLog, type, msg) michael@0: #else michael@0: #define SEEK_LOG(type, msg) michael@0: #endif michael@0: #else michael@0: #define LOG(type, msg) michael@0: #define SEEK_LOG(type, msg) michael@0: #endif michael@0: michael@0: // The number of microseconds of "fuzz" we use in a bisection search over michael@0: // HTTP. When we're seeking with fuzz, we'll stop the search if a bisection michael@0: // lands between the seek target and SEEK_FUZZ_USECS microseconds before the michael@0: // seek target. This is becaue it's usually quicker to just keep downloading michael@0: // from an exisiting connection than to do another bisection inside that michael@0: // small range, which would open a new HTTP connetion. michael@0: static const uint32_t SEEK_FUZZ_USECS = 500000; michael@0: michael@0: // The number of microseconds of "pre-roll" we use for Opus streams. michael@0: // The specification recommends 80 ms. michael@0: #ifdef MOZ_OPUS michael@0: static const int64_t SEEK_OPUS_PREROLL = 80 * USECS_PER_MS; michael@0: #endif /* MOZ_OPUS */ michael@0: michael@0: enum PageSyncResult { michael@0: PAGE_SYNC_ERROR = 1, michael@0: PAGE_SYNC_END_OF_RANGE= 2, michael@0: PAGE_SYNC_OK = 3 michael@0: }; michael@0: michael@0: // Reads a page from the media resource. michael@0: static PageSyncResult michael@0: PageSync(MediaResource* aResource, michael@0: ogg_sync_state* aState, michael@0: bool aCachedDataOnly, michael@0: int64_t aOffset, michael@0: int64_t aEndOffset, michael@0: ogg_page* aPage, michael@0: int& aSkippedBytes); michael@0: michael@0: // Chunk size to read when reading Ogg files. Average Ogg page length michael@0: // is about 4300 bytes, so we read the file in chunks larger than that. michael@0: static const int PAGE_STEP = 8192; michael@0: michael@0: OggReader::OggReader(AbstractMediaDecoder* aDecoder) michael@0: : MediaDecoderReader(aDecoder), michael@0: mMonitor("OggReader"), michael@0: mTheoraState(nullptr), michael@0: mVorbisState(nullptr), michael@0: #ifdef MOZ_OPUS michael@0: mOpusState(nullptr), michael@0: mOpusEnabled(MediaDecoder::IsOpusEnabled()), michael@0: #endif /* MOZ_OPUS */ michael@0: mSkeletonState(nullptr), michael@0: mVorbisSerial(0), michael@0: mOpusSerial(0), michael@0: mTheoraSerial(0), michael@0: mOpusPreSkip(0), michael@0: mIsChained(false), michael@0: mDecodedAudioFrames(0) michael@0: { michael@0: MOZ_COUNT_CTOR(OggReader); michael@0: memset(&mTheoraInfo, 0, sizeof(mTheoraInfo)); michael@0: } michael@0: michael@0: OggReader::~OggReader() michael@0: { michael@0: ogg_sync_clear(&mOggState); michael@0: MOZ_COUNT_DTOR(OggReader); michael@0: } michael@0: michael@0: nsresult OggReader::Init(MediaDecoderReader* aCloneDonor) { michael@0: int ret = ogg_sync_init(&mOggState); michael@0: NS_ENSURE_TRUE(ret == 0, NS_ERROR_FAILURE); michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult OggReader::ResetDecode() michael@0: { michael@0: return ResetDecode(false); michael@0: } michael@0: michael@0: nsresult OggReader::ResetDecode(bool start) michael@0: { michael@0: NS_ASSERTION(mDecoder->OnDecodeThread(), "Should be on decode thread."); michael@0: nsresult res = NS_OK; michael@0: michael@0: if (NS_FAILED(MediaDecoderReader::ResetDecode())) { michael@0: res = NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: // Discard any previously buffered packets/pages. michael@0: ogg_sync_reset(&mOggState); michael@0: if (mVorbisState && NS_FAILED(mVorbisState->Reset())) { michael@0: res = NS_ERROR_FAILURE; michael@0: } michael@0: #ifdef MOZ_OPUS michael@0: if (mOpusState && NS_FAILED(mOpusState->Reset(start))) { michael@0: res = NS_ERROR_FAILURE; michael@0: } michael@0: #endif /* MOZ_OPUS */ michael@0: if (mTheoraState && NS_FAILED(mTheoraState->Reset())) { michael@0: res = NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: return res; michael@0: } michael@0: michael@0: bool OggReader::ReadHeaders(OggCodecState* aState) michael@0: { michael@0: while (!aState->DoneReadingHeaders()) { michael@0: ogg_packet* packet = NextOggPacket(aState); michael@0: // DecodeHeader is responsible for releasing packet. michael@0: if (!packet || !aState->DecodeHeader(packet)) { michael@0: aState->Deactivate(); michael@0: return false; michael@0: } michael@0: } michael@0: return aState->Init(); michael@0: } michael@0: michael@0: void OggReader::BuildSerialList(nsTArray& aTracks) michael@0: { michael@0: if (HasVideo()) { michael@0: aTracks.AppendElement(mTheoraState->mSerial); michael@0: } michael@0: if (HasAudio()) { michael@0: if (mVorbisState) { michael@0: aTracks.AppendElement(mVorbisState->mSerial); michael@0: #ifdef MOZ_OPUS michael@0: } else if (mOpusState) { michael@0: aTracks.AppendElement(mOpusState->mSerial); michael@0: #endif /* MOZ_OPUS */ michael@0: } michael@0: } michael@0: } michael@0: michael@0: nsresult OggReader::ReadMetadata(MediaInfo* aInfo, michael@0: MetadataTags** aTags) michael@0: { michael@0: NS_ASSERTION(mDecoder->OnDecodeThread(), "Should be on decode thread."); michael@0: michael@0: // We read packets until all bitstreams have read all their header packets. michael@0: // We record the offset of the first non-header page so that we know michael@0: // what page to seek to when seeking to the media start. michael@0: michael@0: NS_ASSERTION(aTags, "Called with null MetadataTags**."); michael@0: *aTags = nullptr; michael@0: michael@0: ogg_page page; michael@0: nsAutoTArray bitstreams; michael@0: bool readAllBOS = false; michael@0: while (!readAllBOS) { michael@0: if (!ReadOggPage(&page)) { michael@0: // Some kind of error... michael@0: break; michael@0: } michael@0: michael@0: int serial = ogg_page_serialno(&page); michael@0: OggCodecState* codecState = 0; michael@0: michael@0: if (!ogg_page_bos(&page)) { michael@0: // We've encountered a non Beginning Of Stream page. No more BOS pages michael@0: // can follow in this Ogg segment, so there will be no other bitstreams michael@0: // in the Ogg (unless it's invalid). michael@0: readAllBOS = true; michael@0: } else if (!mCodecStore.Contains(serial)) { michael@0: // We've not encountered a stream with this serial number before. Create michael@0: // an OggCodecState to demux it, and map that to the OggCodecState michael@0: // in mCodecStates. michael@0: codecState = OggCodecState::Create(&page); michael@0: mCodecStore.Add(serial, codecState); michael@0: bitstreams.AppendElement(codecState); michael@0: if (codecState && michael@0: codecState->GetType() == OggCodecState::TYPE_VORBIS && michael@0: !mVorbisState) michael@0: { michael@0: // First Vorbis bitstream, we'll play this one. Subsequent Vorbis michael@0: // bitstreams will be ignored. michael@0: mVorbisState = static_cast(codecState); michael@0: } michael@0: if (codecState && michael@0: codecState->GetType() == OggCodecState::TYPE_THEORA && michael@0: !mTheoraState) michael@0: { michael@0: // First Theora bitstream, we'll play this one. Subsequent Theora michael@0: // bitstreams will be ignored. michael@0: mTheoraState = static_cast(codecState); michael@0: } michael@0: #ifdef MOZ_OPUS michael@0: if (codecState && michael@0: codecState->GetType() == OggCodecState::TYPE_OPUS && michael@0: !mOpusState) michael@0: { michael@0: if (mOpusEnabled) { michael@0: mOpusState = static_cast(codecState); michael@0: } else { michael@0: NS_WARNING("Opus decoding disabled." michael@0: " See media.opus.enabled in about:config"); michael@0: } michael@0: } michael@0: #endif /* MOZ_OPUS */ michael@0: if (codecState && michael@0: codecState->GetType() == OggCodecState::TYPE_SKELETON && michael@0: !mSkeletonState) michael@0: { michael@0: mSkeletonState = static_cast(codecState); michael@0: } michael@0: } michael@0: michael@0: codecState = mCodecStore.Get(serial); michael@0: NS_ENSURE_TRUE(codecState != nullptr, NS_ERROR_FAILURE); michael@0: michael@0: if (NS_FAILED(codecState->PageIn(&page))) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: } michael@0: michael@0: // We've read all BOS pages, so we know the streams contained in the media. michael@0: // Now process all available header packets in the active Theora, Vorbis and michael@0: // Skeleton streams. michael@0: michael@0: // Deactivate any non-primary bitstreams. michael@0: for (uint32_t i = 0; i < bitstreams.Length(); i++) { michael@0: OggCodecState* s = bitstreams[i]; michael@0: if (s != mVorbisState && michael@0: #ifdef MOZ_OPUS michael@0: s != mOpusState && michael@0: #endif /* MOZ_OPUS */ michael@0: s != mTheoraState && s != mSkeletonState) { michael@0: s->Deactivate(); michael@0: } michael@0: } michael@0: michael@0: if (mTheoraState && ReadHeaders(mTheoraState)) { michael@0: nsIntRect picture = nsIntRect(mTheoraState->mInfo.pic_x, michael@0: mTheoraState->mInfo.pic_y, michael@0: mTheoraState->mInfo.pic_width, michael@0: mTheoraState->mInfo.pic_height); michael@0: michael@0: nsIntSize displaySize = nsIntSize(mTheoraState->mInfo.pic_width, michael@0: mTheoraState->mInfo.pic_height); michael@0: michael@0: // Apply the aspect ratio to produce the intrinsic display size we report michael@0: // to the element. michael@0: ScaleDisplayByAspectRatio(displaySize, mTheoraState->mPixelAspectRatio); michael@0: michael@0: nsIntSize frameSize(mTheoraState->mInfo.frame_width, michael@0: mTheoraState->mInfo.frame_height); michael@0: if (IsValidVideoRegion(frameSize, picture, displaySize)) { michael@0: // Video track's frame sizes will not overflow. Activate the video track. michael@0: mInfo.mVideo.mHasVideo = true; michael@0: mInfo.mVideo.mDisplay = displaySize; michael@0: mPicture = picture; michael@0: michael@0: VideoFrameContainer* container = mDecoder->GetVideoFrameContainer(); michael@0: if (container) { michael@0: container->SetCurrentFrame(gfxIntSize(displaySize.width, displaySize.height), michael@0: nullptr, michael@0: TimeStamp::Now()); michael@0: } michael@0: michael@0: // Copy Theora info data for time computations on other threads. michael@0: memcpy(&mTheoraInfo, &mTheoraState->mInfo, sizeof(mTheoraInfo)); michael@0: mTheoraSerial = mTheoraState->mSerial; michael@0: } michael@0: } michael@0: michael@0: if (mVorbisState && ReadHeaders(mVorbisState)) { michael@0: mInfo.mAudio.mHasAudio = true; michael@0: mInfo.mAudio.mRate = mVorbisState->mInfo.rate; michael@0: mInfo.mAudio.mChannels = mVorbisState->mInfo.channels; michael@0: // Copy Vorbis info data for time computations on other threads. michael@0: memcpy(&mVorbisInfo, &mVorbisState->mInfo, sizeof(mVorbisInfo)); michael@0: mVorbisInfo.codec_setup = nullptr; michael@0: mVorbisSerial = mVorbisState->mSerial; michael@0: *aTags = mVorbisState->GetTags(); michael@0: } else { michael@0: memset(&mVorbisInfo, 0, sizeof(mVorbisInfo)); michael@0: } michael@0: #ifdef MOZ_OPUS michael@0: if (mOpusState && ReadHeaders(mOpusState)) { michael@0: mInfo.mAudio.mHasAudio = true; michael@0: mInfo.mAudio.mRate = mOpusState->mRate; michael@0: mInfo.mAudio.mChannels = mOpusState->mChannels; michael@0: mOpusSerial = mOpusState->mSerial; michael@0: mOpusPreSkip = mOpusState->mPreSkip; michael@0: michael@0: *aTags = mOpusState->GetTags(); michael@0: } michael@0: #endif michael@0: if (mSkeletonState) { michael@0: if (!HasAudio() && !HasVideo()) { michael@0: // We have a skeleton track, but no audio or video, may as well disable michael@0: // the skeleton, we can't do anything useful with this media. michael@0: mSkeletonState->Deactivate(); michael@0: } else if (ReadHeaders(mSkeletonState) && mSkeletonState->HasIndex()) { michael@0: // Extract the duration info out of the index, so we don't need to seek to michael@0: // the end of resource to get it. michael@0: nsAutoTArray tracks; michael@0: BuildSerialList(tracks); michael@0: int64_t duration = 0; michael@0: if (NS_SUCCEEDED(mSkeletonState->GetDuration(tracks, duration))) { michael@0: ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); michael@0: mDecoder->SetMediaDuration(duration); michael@0: LOG(PR_LOG_DEBUG, ("Got duration from Skeleton index %lld", duration)); michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (HasAudio() || HasVideo()) { michael@0: ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); michael@0: michael@0: MediaResource* resource = mDecoder->GetResource(); michael@0: if (mDecoder->GetMediaDuration() == -1 && michael@0: !mDecoder->IsShutdown() && michael@0: resource->GetLength() >= 0 && michael@0: mDecoder->IsMediaSeekable()) michael@0: { michael@0: // We didn't get a duration from the index or a Content-Duration header. michael@0: // Seek to the end of file to find the end time. michael@0: mDecoder->GetResource()->StartSeekingForMetadata(); michael@0: int64_t length = resource->GetLength(); michael@0: michael@0: NS_ASSERTION(length > 0, "Must have a content length to get end time"); michael@0: michael@0: int64_t endTime = 0; michael@0: { michael@0: ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor()); michael@0: endTime = RangeEndTime(length); michael@0: } michael@0: if (endTime != -1) { michael@0: mDecoder->SetMediaEndTime(endTime); michael@0: LOG(PR_LOG_DEBUG, ("Got Ogg duration from seeking to end %lld", endTime)); michael@0: } michael@0: mDecoder->GetResource()->EndSeekingForMetadata(); michael@0: } else if (mDecoder->GetMediaDuration() == -1) { michael@0: // We don't have a duration, and we don't know enough about the resource michael@0: // to try a seek. Abort trying to get a duration. This happens for example michael@0: // when the server says it accepts range requests, but does not give us a michael@0: // Content-Length. michael@0: mDecoder->SetTransportSeekable(false); michael@0: } michael@0: } else { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: *aInfo = mInfo; michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult OggReader::DecodeVorbis(ogg_packet* aPacket) { michael@0: NS_ASSERTION(aPacket->granulepos != -1, "Must know vorbis granulepos!"); michael@0: michael@0: if (vorbis_synthesis(&mVorbisState->mBlock, aPacket) != 0) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: if (vorbis_synthesis_blockin(&mVorbisState->mDsp, michael@0: &mVorbisState->mBlock) != 0) michael@0: { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: VorbisPCMValue** pcm = 0; michael@0: int32_t frames = 0; michael@0: uint32_t channels = mVorbisState->mInfo.channels; michael@0: ogg_int64_t endFrame = aPacket->granulepos; michael@0: while ((frames = vorbis_synthesis_pcmout(&mVorbisState->mDsp, &pcm)) > 0) { michael@0: mVorbisState->ValidateVorbisPacketSamples(aPacket, frames); michael@0: nsAutoArrayPtr buffer(new AudioDataValue[frames * channels]); michael@0: for (uint32_t j = 0; j < channels; ++j) { michael@0: VorbisPCMValue* channel = pcm[j]; michael@0: for (uint32_t i = 0; i < uint32_t(frames); ++i) { michael@0: buffer[i*channels + j] = MOZ_CONVERT_VORBIS_SAMPLE(channel[i]); michael@0: } michael@0: } michael@0: michael@0: // No channel mapping for more than 8 channels. michael@0: if (channels > 8) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: int64_t duration = mVorbisState->Time((int64_t)frames); michael@0: int64_t startTime = mVorbisState->Time(endFrame - frames); michael@0: mAudioQueue.Push(new AudioData(mDecoder->GetResource()->Tell(), michael@0: startTime, michael@0: duration, michael@0: frames, michael@0: buffer.forget(), michael@0: channels)); michael@0: michael@0: mDecodedAudioFrames += frames; michael@0: michael@0: endFrame -= frames; michael@0: if (vorbis_synthesis_read(&mVorbisState->mDsp, frames) != 0) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: } michael@0: return NS_OK; michael@0: } michael@0: #ifdef MOZ_OPUS michael@0: nsresult OggReader::DecodeOpus(ogg_packet* aPacket) { michael@0: NS_ASSERTION(aPacket->granulepos != -1, "Must know opus granulepos!"); michael@0: michael@0: // Maximum value is 63*2880, so there's no chance of overflow. michael@0: int32_t frames_number = opus_packet_get_nb_frames(aPacket->packet, michael@0: aPacket->bytes); michael@0: if (frames_number <= 0) michael@0: return NS_ERROR_FAILURE; // Invalid packet header. michael@0: int32_t samples = opus_packet_get_samples_per_frame(aPacket->packet, michael@0: (opus_int32) mOpusState->mRate); michael@0: int32_t frames = frames_number*samples; michael@0: michael@0: // A valid Opus packet must be between 2.5 and 120 ms long. michael@0: if (frames < 120 || frames > 5760) michael@0: return NS_ERROR_FAILURE; michael@0: uint32_t channels = mOpusState->mChannels; michael@0: nsAutoArrayPtr buffer(new AudioDataValue[frames * channels]); michael@0: michael@0: // Decode to the appropriate sample type. michael@0: #ifdef MOZ_SAMPLE_TYPE_FLOAT32 michael@0: int ret = opus_multistream_decode_float(mOpusState->mDecoder, michael@0: aPacket->packet, aPacket->bytes, michael@0: buffer, frames, false); michael@0: #else michael@0: int ret = opus_multistream_decode(mOpusState->mDecoder, michael@0: aPacket->packet, aPacket->bytes, michael@0: buffer, frames, false); michael@0: #endif michael@0: if (ret < 0) michael@0: return NS_ERROR_FAILURE; michael@0: NS_ASSERTION(ret == frames, "Opus decoded too few audio samples"); michael@0: michael@0: int64_t endFrame = aPacket->granulepos; michael@0: int64_t startFrame; michael@0: // If this is the last packet, perform end trimming. michael@0: if (aPacket->e_o_s && mOpusState->mPrevPacketGranulepos != -1) { michael@0: startFrame = mOpusState->mPrevPacketGranulepos; michael@0: frames = static_cast(std::max(static_cast(0), michael@0: std::min(endFrame - startFrame, michael@0: static_cast(frames)))); michael@0: } else { michael@0: startFrame = endFrame - frames; michael@0: } michael@0: michael@0: // Trim the initial frames while the decoder is settling. michael@0: if (mOpusState->mSkip > 0) { michael@0: int32_t skipFrames = std::min(mOpusState->mSkip, frames); michael@0: if (skipFrames == frames) { michael@0: // discard the whole packet michael@0: mOpusState->mSkip -= frames; michael@0: LOG(PR_LOG_DEBUG, ("Opus decoder skipping %d frames" michael@0: " (whole packet)", frames)); michael@0: return NS_OK; michael@0: } michael@0: int32_t keepFrames = frames - skipFrames; michael@0: int samples = keepFrames * channels; michael@0: nsAutoArrayPtr trimBuffer(new AudioDataValue[samples]); michael@0: for (int i = 0; i < samples; i++) michael@0: trimBuffer[i] = buffer[skipFrames*channels + i]; michael@0: michael@0: startFrame = endFrame - keepFrames; michael@0: frames = keepFrames; michael@0: buffer = trimBuffer; michael@0: michael@0: mOpusState->mSkip -= skipFrames; michael@0: LOG(PR_LOG_DEBUG, ("Opus decoder skipping %d frames", skipFrames)); michael@0: } michael@0: // Save this packet's granule position in case we need to perform end michael@0: // trimming on the next packet. michael@0: mOpusState->mPrevPacketGranulepos = endFrame; michael@0: michael@0: // Apply the header gain if one was specified. michael@0: #ifdef MOZ_SAMPLE_TYPE_FLOAT32 michael@0: if (mOpusState->mGain != 1.0f) { michael@0: float gain = mOpusState->mGain; michael@0: int samples = frames * channels; michael@0: for (int i = 0; i < samples; i++) { michael@0: buffer[i] *= gain; michael@0: } michael@0: } michael@0: #else michael@0: if (mOpusState->mGain_Q16 != 65536) { michael@0: int64_t gain_Q16 = mOpusState->mGain_Q16; michael@0: int samples = frames * channels; michael@0: for (int i = 0; i < samples; i++) { michael@0: int32_t val = static_cast((gain_Q16*buffer[i] + 32768)>>16); michael@0: buffer[i] = static_cast(MOZ_CLIP_TO_15(val)); michael@0: } michael@0: } michael@0: #endif michael@0: michael@0: // No channel mapping for more than 8 channels. michael@0: if (channels > 8) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: LOG(PR_LOG_DEBUG, ("Opus decoder pushing %d frames", frames)); michael@0: int64_t startTime = mOpusState->Time(startFrame); michael@0: int64_t endTime = mOpusState->Time(endFrame); michael@0: mAudioQueue.Push(new AudioData(mDecoder->GetResource()->Tell(), michael@0: startTime, michael@0: endTime - startTime, michael@0: frames, michael@0: buffer.forget(), michael@0: channels)); michael@0: michael@0: mDecodedAudioFrames += frames; michael@0: michael@0: return NS_OK; michael@0: } michael@0: #endif /* MOZ_OPUS */ michael@0: michael@0: bool OggReader::DecodeAudioData() michael@0: { michael@0: NS_ASSERTION(mDecoder->OnDecodeThread(), "Should be on decode thread."); michael@0: DebugOnly haveCodecState = mVorbisState != nullptr michael@0: #ifdef MOZ_OPUS michael@0: || mOpusState != nullptr michael@0: #endif /* MOZ_OPUS */ michael@0: ; michael@0: NS_ASSERTION(haveCodecState, "Need audio codec state to decode audio"); michael@0: michael@0: // Read the next data packet. Skip any non-data packets we encounter. michael@0: ogg_packet* packet = 0; michael@0: OggCodecState* codecState; michael@0: if (mVorbisState) michael@0: codecState = static_cast(mVorbisState); michael@0: #ifdef MOZ_OPUS michael@0: else michael@0: codecState = static_cast(mOpusState); michael@0: #endif /* MOZ_OPUS */ michael@0: do { michael@0: if (packet) { michael@0: OggCodecState::ReleasePacket(packet); michael@0: } michael@0: packet = NextOggPacket(codecState); michael@0: } while (packet && codecState->IsHeader(packet)); michael@0: michael@0: if (!packet) { michael@0: return false; michael@0: } michael@0: michael@0: NS_ASSERTION(packet && packet->granulepos != -1, michael@0: "Must have packet with known granulepos"); michael@0: nsAutoRef autoRelease(packet); michael@0: if (mVorbisState) { michael@0: DecodeVorbis(packet); michael@0: #ifdef MOZ_OPUS michael@0: } else if (mOpusState) { michael@0: DecodeOpus(packet); michael@0: #endif michael@0: } michael@0: michael@0: if ((packet->e_o_s) && (!ReadOggChain())) { michael@0: // We've encountered an end of bitstream packet, or we've hit the end of michael@0: // file while trying to decode, so inform the audio queue that there'll michael@0: // be no more samples. michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: void OggReader::SetChained(bool aIsChained) { michael@0: { michael@0: ReentrantMonitorAutoEnter mon(mMonitor); michael@0: mIsChained = aIsChained; michael@0: } michael@0: { michael@0: ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); michael@0: mDecoder->SetMediaSeekable(false); michael@0: } michael@0: } michael@0: michael@0: bool OggReader::ReadOggChain() michael@0: { michael@0: bool chained = false; michael@0: #ifdef MOZ_OPUS michael@0: OpusState* newOpusState = nullptr; michael@0: #endif /* MOZ_OPUS */ michael@0: VorbisState* newVorbisState = nullptr; michael@0: int channels = 0; michael@0: long rate = 0; michael@0: MetadataTags* tags = nullptr; michael@0: michael@0: if (HasVideo() || HasSkeleton() || !HasAudio()) { michael@0: return false; michael@0: } michael@0: michael@0: ogg_page page; michael@0: if (!ReadOggPage(&page) || !ogg_page_bos(&page)) { michael@0: return false; michael@0: } michael@0: michael@0: int serial = ogg_page_serialno(&page); michael@0: if (mCodecStore.Contains(serial)) { michael@0: return false; michael@0: } michael@0: michael@0: nsAutoPtr codecState; michael@0: codecState = OggCodecState::Create(&page); michael@0: if (!codecState) { michael@0: return false; michael@0: } michael@0: michael@0: if (mVorbisState && (codecState->GetType() == OggCodecState::TYPE_VORBIS)) { michael@0: newVorbisState = static_cast(codecState.get()); michael@0: } michael@0: #ifdef MOZ_OPUS michael@0: else if (mOpusState && (codecState->GetType() == OggCodecState::TYPE_OPUS)) { michael@0: newOpusState = static_cast(codecState.get()); michael@0: } michael@0: #endif michael@0: else { michael@0: return false; michael@0: } michael@0: OggCodecState* state; michael@0: michael@0: mCodecStore.Add(serial, codecState.forget()); michael@0: state = mCodecStore.Get(serial); michael@0: michael@0: NS_ENSURE_TRUE(state != nullptr, false); michael@0: michael@0: if (NS_FAILED(state->PageIn(&page))) { michael@0: return false; michael@0: } michael@0: michael@0: if ((newVorbisState && ReadHeaders(newVorbisState)) && michael@0: (mVorbisState->mInfo.rate == newVorbisState->mInfo.rate) && michael@0: (mVorbisState->mInfo.channels == newVorbisState->mInfo.channels)) { michael@0: mVorbisState->Reset(); michael@0: mVorbisState = newVorbisState; michael@0: mVorbisSerial = mVorbisState->mSerial; michael@0: LOG(PR_LOG_DEBUG, ("New vorbis ogg link, serial=%d\n", mVorbisSerial)); michael@0: chained = true; michael@0: rate = mVorbisState->mInfo.rate; michael@0: channels = mVorbisState->mInfo.channels; michael@0: tags = mVorbisState->GetTags(); michael@0: } michael@0: michael@0: #ifdef MOZ_OPUS michael@0: if ((newOpusState && ReadHeaders(newOpusState)) && michael@0: (mOpusState->mRate == newOpusState->mRate) && michael@0: (mOpusState->mChannels == newOpusState->mChannels)) { michael@0: mOpusState->Reset(); michael@0: mOpusState = newOpusState; michael@0: mOpusSerial = mOpusState->mSerial; michael@0: chained = true; michael@0: rate = mOpusState->mRate; michael@0: channels = mOpusState->mChannels; michael@0: tags = mOpusState->GetTags(); michael@0: } michael@0: #endif michael@0: michael@0: if (chained) { michael@0: SetChained(true); michael@0: { michael@0: ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); michael@0: mDecoder->QueueMetadata((mDecodedAudioFrames * USECS_PER_S) / rate, michael@0: channels, michael@0: rate, michael@0: HasAudio(), michael@0: HasVideo(), michael@0: tags); michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: nsresult OggReader::DecodeTheora(ogg_packet* aPacket, int64_t aTimeThreshold) michael@0: { michael@0: NS_ASSERTION(aPacket->granulepos >= TheoraVersion(&mTheoraState->mInfo,3,2,1), michael@0: "Packets must have valid granulepos and packetno"); michael@0: michael@0: int ret = th_decode_packetin(mTheoraState->mCtx, aPacket, 0); michael@0: if (ret != 0 && ret != TH_DUPFRAME) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: int64_t time = mTheoraState->StartTime(aPacket->granulepos); michael@0: michael@0: // Don't use the frame if it's outside the bounds of the presentation michael@0: // start time in the skeleton track. Note we still must submit the frame michael@0: // to the decoder (via th_decode_packetin), as the frames which are michael@0: // presentable may depend on this frame's data. michael@0: if (mSkeletonState && !mSkeletonState->IsPresentable(time)) { michael@0: return NS_OK; michael@0: } michael@0: michael@0: int64_t endTime = mTheoraState->Time(aPacket->granulepos); michael@0: if (endTime < aTimeThreshold) { michael@0: // The end time of this frame is already before the current playback michael@0: // position. It will never be displayed, don't bother enqueing it. michael@0: return NS_OK; michael@0: } michael@0: michael@0: if (ret == TH_DUPFRAME) { michael@0: VideoData* v = VideoData::CreateDuplicate(mDecoder->GetResource()->Tell(), michael@0: time, michael@0: endTime - time, michael@0: aPacket->granulepos); michael@0: mVideoQueue.Push(v); michael@0: } else if (ret == 0) { michael@0: th_ycbcr_buffer buffer; michael@0: ret = th_decode_ycbcr_out(mTheoraState->mCtx, buffer); michael@0: NS_ASSERTION(ret == 0, "th_decode_ycbcr_out failed"); michael@0: bool isKeyframe = th_packet_iskeyframe(aPacket) == 1; michael@0: VideoData::YCbCrBuffer b; michael@0: for (uint32_t i=0; i < 3; ++i) { michael@0: b.mPlanes[i].mData = buffer[i].data; michael@0: b.mPlanes[i].mHeight = buffer[i].height; michael@0: b.mPlanes[i].mWidth = buffer[i].width; michael@0: b.mPlanes[i].mStride = buffer[i].stride; michael@0: b.mPlanes[i].mOffset = b.mPlanes[i].mSkip = 0; michael@0: } michael@0: michael@0: VideoData *v = VideoData::Create(mInfo.mVideo, michael@0: mDecoder->GetImageContainer(), michael@0: mDecoder->GetResource()->Tell(), michael@0: time, michael@0: endTime - time, michael@0: b, michael@0: isKeyframe, michael@0: aPacket->granulepos, michael@0: ToIntRect(mPicture)); michael@0: if (!v) { michael@0: // There may be other reasons for this error, but for michael@0: // simplicity just assume the worst case: out of memory. michael@0: NS_WARNING("Failed to allocate memory for video frame"); michael@0: return NS_ERROR_OUT_OF_MEMORY; michael@0: } michael@0: mVideoQueue.Push(v); michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: bool OggReader::DecodeVideoFrame(bool &aKeyframeSkip, michael@0: int64_t aTimeThreshold) michael@0: { michael@0: NS_ASSERTION(mDecoder->OnDecodeThread(), "Should be on decode thread."); michael@0: michael@0: // Record number of frames decoded and parsed. Automatically update the michael@0: // stats counters using the AutoNotifyDecoded stack-based class. michael@0: uint32_t parsed = 0, decoded = 0; michael@0: AbstractMediaDecoder::AutoNotifyDecoded autoNotify(mDecoder, parsed, decoded); michael@0: michael@0: // Read the next data packet. Skip any non-data packets we encounter. michael@0: ogg_packet* packet = 0; michael@0: do { michael@0: if (packet) { michael@0: OggCodecState::ReleasePacket(packet); michael@0: } michael@0: packet = NextOggPacket(mTheoraState); michael@0: } while (packet && mTheoraState->IsHeader(packet)); michael@0: if (!packet) { michael@0: return false; michael@0: } michael@0: nsAutoRef autoRelease(packet); michael@0: michael@0: parsed++; michael@0: NS_ASSERTION(packet && packet->granulepos != -1, michael@0: "Must know first packet's granulepos"); michael@0: bool eos = packet->e_o_s; michael@0: int64_t frameEndTime = mTheoraState->Time(packet->granulepos); michael@0: if (!aKeyframeSkip || michael@0: (th_packet_iskeyframe(packet) && frameEndTime >= aTimeThreshold)) michael@0: { michael@0: aKeyframeSkip = false; michael@0: nsresult res = DecodeTheora(packet, aTimeThreshold); michael@0: decoded++; michael@0: if (NS_FAILED(res)) { michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: if (eos) { michael@0: // We've encountered an end of bitstream packet. Inform the queue that michael@0: // there will be no more frames. michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: bool OggReader::ReadOggPage(ogg_page* aPage) michael@0: { michael@0: NS_ASSERTION(mDecoder->OnDecodeThread(), "Should be on decode thread."); michael@0: michael@0: int ret = 0; michael@0: while((ret = ogg_sync_pageseek(&mOggState, aPage)) <= 0) { michael@0: if (ret < 0) { michael@0: // Lost page sync, have to skip up to next page. michael@0: continue; michael@0: } michael@0: // Returns a buffer that can be written too michael@0: // with the given size. This buffer is stored michael@0: // in the ogg synchronisation structure. michael@0: char* buffer = ogg_sync_buffer(&mOggState, 4096); michael@0: NS_ASSERTION(buffer, "ogg_sync_buffer failed"); michael@0: michael@0: // Read from the resource into the buffer michael@0: uint32_t bytesRead = 0; michael@0: michael@0: nsresult rv = mDecoder->GetResource()->Read(buffer, 4096, &bytesRead); michael@0: if (NS_FAILED(rv) || (bytesRead == 0 && ret == 0)) { michael@0: // End of file. michael@0: return false; michael@0: } michael@0: michael@0: // Update the synchronisation layer with the number michael@0: // of bytes written to the buffer michael@0: ret = ogg_sync_wrote(&mOggState, bytesRead); michael@0: NS_ENSURE_TRUE(ret == 0, false); michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: ogg_packet* OggReader::NextOggPacket(OggCodecState* aCodecState) michael@0: { michael@0: NS_ASSERTION(mDecoder->OnDecodeThread(), "Should be on decode thread."); michael@0: michael@0: if (!aCodecState || !aCodecState->mActive) { michael@0: return nullptr; michael@0: } michael@0: michael@0: ogg_packet* packet; michael@0: while ((packet = aCodecState->PacketOut()) == nullptr) { michael@0: // The codec state does not have any buffered pages, so try to read another michael@0: // page from the channel. michael@0: ogg_page page; michael@0: if (!ReadOggPage(&page)) { michael@0: return nullptr; michael@0: } michael@0: michael@0: uint32_t serial = ogg_page_serialno(&page); michael@0: OggCodecState* codecState = nullptr; michael@0: codecState = mCodecStore.Get(serial); michael@0: if (codecState && NS_FAILED(codecState->PageIn(&page))) { michael@0: return nullptr; michael@0: } michael@0: } michael@0: michael@0: return packet; michael@0: } michael@0: michael@0: // Returns an ogg page's checksum. michael@0: static ogg_uint32_t michael@0: GetChecksum(ogg_page* page) michael@0: { michael@0: if (page == 0 || page->header == 0 || page->header_len < 25) { michael@0: return 0; michael@0: } michael@0: const unsigned char* p = page->header + 22; michael@0: uint32_t c = p[0] + michael@0: (p[1] << 8) + michael@0: (p[2] << 16) + michael@0: (p[3] << 24); michael@0: return c; michael@0: } michael@0: michael@0: int64_t OggReader::RangeStartTime(int64_t aOffset) michael@0: { michael@0: NS_ASSERTION(mDecoder->OnDecodeThread(), "Should be on decode thread."); michael@0: MediaResource* resource = mDecoder->GetResource(); michael@0: NS_ENSURE_TRUE(resource != nullptr, 0); michael@0: nsresult res = resource->Seek(nsISeekableStream::NS_SEEK_SET, aOffset); michael@0: NS_ENSURE_SUCCESS(res, 0); michael@0: int64_t startTime = 0; michael@0: MediaDecoderReader::FindStartTime(startTime); michael@0: return startTime; michael@0: } michael@0: michael@0: struct nsAutoOggSyncState { michael@0: nsAutoOggSyncState() { michael@0: ogg_sync_init(&mState); michael@0: } michael@0: ~nsAutoOggSyncState() { michael@0: ogg_sync_clear(&mState); michael@0: } michael@0: ogg_sync_state mState; michael@0: }; michael@0: michael@0: int64_t OggReader::RangeEndTime(int64_t aEndOffset) michael@0: { michael@0: NS_ASSERTION(mDecoder->OnStateMachineThread() || mDecoder->OnDecodeThread(), michael@0: "Should be on state machine or decode thread."); michael@0: michael@0: MediaResource* resource = mDecoder->GetResource(); michael@0: NS_ENSURE_TRUE(resource != nullptr, -1); michael@0: int64_t position = resource->Tell(); michael@0: int64_t endTime = RangeEndTime(0, aEndOffset, false); michael@0: nsresult res = resource->Seek(nsISeekableStream::NS_SEEK_SET, position); michael@0: NS_ENSURE_SUCCESS(res, -1); michael@0: return endTime; michael@0: } michael@0: michael@0: int64_t OggReader::RangeEndTime(int64_t aStartOffset, michael@0: int64_t aEndOffset, michael@0: bool aCachedDataOnly) michael@0: { michael@0: MediaResource* resource = mDecoder->GetResource(); michael@0: nsAutoOggSyncState sync; michael@0: michael@0: // We need to find the last page which ends before aEndOffset that michael@0: // has a granulepos that we can convert to a timestamp. We do this by michael@0: // backing off from aEndOffset until we encounter a page on which we can michael@0: // interpret the granulepos. If while backing off we encounter a page which michael@0: // we've previously encountered before, we'll either backoff again if we michael@0: // haven't found an end time yet, or return the last end time found. michael@0: const int step = 5000; michael@0: const int maxOggPageSize = 65306; michael@0: int64_t readStartOffset = aEndOffset; michael@0: int64_t readLimitOffset = aEndOffset; michael@0: int64_t readHead = aEndOffset; michael@0: int64_t endTime = -1; michael@0: uint32_t checksumAfterSeek = 0; michael@0: uint32_t prevChecksumAfterSeek = 0; michael@0: bool mustBackOff = false; michael@0: while (true) { michael@0: ogg_page page; michael@0: int ret = ogg_sync_pageseek(&sync.mState, &page); michael@0: if (ret == 0) { michael@0: // We need more data if we've not encountered a page we've seen before, michael@0: // or we've read to the end of file. michael@0: if (mustBackOff || readHead == aEndOffset || readHead == aStartOffset) { michael@0: if (endTime != -1 || readStartOffset == 0) { michael@0: // We have encountered a page before, or we're at the end of file. michael@0: break; michael@0: } michael@0: mustBackOff = false; michael@0: prevChecksumAfterSeek = checksumAfterSeek; michael@0: checksumAfterSeek = 0; michael@0: ogg_sync_reset(&sync.mState); michael@0: readStartOffset = std::max(static_cast(0), readStartOffset - step); michael@0: // There's no point reading more than the maximum size of michael@0: // an Ogg page into data we've previously scanned. Any data michael@0: // between readLimitOffset and aEndOffset must be garbage michael@0: // and we can ignore it thereafter. michael@0: readLimitOffset = std::min(readLimitOffset, michael@0: readStartOffset + maxOggPageSize); michael@0: readHead = std::max(aStartOffset, readStartOffset); michael@0: } michael@0: michael@0: int64_t limit = std::min(static_cast(UINT32_MAX), michael@0: aEndOffset - readHead); michael@0: limit = std::max(static_cast(0), limit); michael@0: limit = std::min(limit, static_cast(step)); michael@0: uint32_t bytesToRead = static_cast(limit); michael@0: uint32_t bytesRead = 0; michael@0: char* buffer = ogg_sync_buffer(&sync.mState, bytesToRead); michael@0: NS_ASSERTION(buffer, "Must have buffer"); michael@0: nsresult res; michael@0: if (aCachedDataOnly) { michael@0: res = resource->ReadFromCache(buffer, readHead, bytesToRead); michael@0: NS_ENSURE_SUCCESS(res, -1); michael@0: bytesRead = bytesToRead; michael@0: } else { michael@0: NS_ASSERTION(readHead < aEndOffset, michael@0: "resource pos must be before range end"); michael@0: res = resource->Seek(nsISeekableStream::NS_SEEK_SET, readHead); michael@0: NS_ENSURE_SUCCESS(res, -1); michael@0: res = resource->Read(buffer, bytesToRead, &bytesRead); michael@0: NS_ENSURE_SUCCESS(res, -1); michael@0: } michael@0: readHead += bytesRead; michael@0: if (readHead > readLimitOffset) { michael@0: mustBackOff = true; michael@0: } michael@0: michael@0: // Update the synchronisation layer with the number michael@0: // of bytes written to the buffer michael@0: ret = ogg_sync_wrote(&sync.mState, bytesRead); michael@0: if (ret != 0) { michael@0: endTime = -1; michael@0: break; michael@0: } michael@0: michael@0: continue; michael@0: } michael@0: michael@0: if (ret < 0 || ogg_page_granulepos(&page) < 0) { michael@0: continue; michael@0: } michael@0: michael@0: uint32_t checksum = GetChecksum(&page); michael@0: if (checksumAfterSeek == 0) { michael@0: // This is the first page we've decoded after a backoff/seek. Remember michael@0: // the page checksum. If we backoff further and encounter this page michael@0: // again, we'll know that we won't find a page with an end time after michael@0: // this one, so we'll know to back off again. michael@0: checksumAfterSeek = checksum; michael@0: } michael@0: if (checksum == prevChecksumAfterSeek) { michael@0: // This page has the same checksum as the first page we encountered michael@0: // after the last backoff/seek. Since we've already scanned after this michael@0: // page and failed to find an end time, we may as well backoff again and michael@0: // try to find an end time from an earlier page. michael@0: mustBackOff = true; michael@0: continue; michael@0: } michael@0: michael@0: int64_t granulepos = ogg_page_granulepos(&page); michael@0: int serial = ogg_page_serialno(&page); michael@0: michael@0: OggCodecState* codecState = nullptr; michael@0: codecState = mCodecStore.Get(serial); michael@0: michael@0: if (!codecState) { michael@0: // This page is from a bitstream which we haven't encountered yet. michael@0: // It's probably from a new "link" in a "chained" ogg. Don't michael@0: // bother even trying to find a duration... michael@0: SetChained(true); michael@0: endTime = -1; michael@0: break; michael@0: } michael@0: michael@0: int64_t t = codecState->Time(granulepos); michael@0: if (t != -1) { michael@0: endTime = t; michael@0: } michael@0: } michael@0: michael@0: return endTime; michael@0: } michael@0: michael@0: nsresult OggReader::GetSeekRanges(nsTArray& aRanges) michael@0: { michael@0: NS_ASSERTION(mDecoder->OnDecodeThread(), "Should be on decode thread."); michael@0: nsTArray cached; michael@0: nsresult res = mDecoder->GetResource()->GetCachedRanges(cached); michael@0: NS_ENSURE_SUCCESS(res, res); michael@0: michael@0: for (uint32_t index = 0; index < cached.Length(); index++) { michael@0: MediaByteRange& range = cached[index]; michael@0: int64_t startTime = -1; michael@0: int64_t endTime = -1; michael@0: if (NS_FAILED(ResetDecode())) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: int64_t startOffset = range.mStart; michael@0: int64_t endOffset = range.mEnd; michael@0: startTime = RangeStartTime(startOffset); michael@0: if (startTime != -1 && michael@0: ((endTime = RangeEndTime(endOffset)) != -1)) michael@0: { michael@0: NS_WARN_IF_FALSE(startTime < endTime, michael@0: "Start time must be before end time"); michael@0: aRanges.AppendElement(SeekRange(startOffset, michael@0: endOffset, michael@0: startTime, michael@0: endTime)); michael@0: } michael@0: } michael@0: if (NS_FAILED(ResetDecode())) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: OggReader::SeekRange michael@0: OggReader::SelectSeekRange(const nsTArray& ranges, michael@0: int64_t aTarget, michael@0: int64_t aStartTime, michael@0: int64_t aEndTime, michael@0: bool aExact) michael@0: { michael@0: NS_ASSERTION(mDecoder->OnDecodeThread(), "Should be on decode thread."); michael@0: int64_t so = 0; michael@0: int64_t eo = mDecoder->GetResource()->GetLength(); michael@0: int64_t st = aStartTime; michael@0: int64_t et = aEndTime; michael@0: for (uint32_t i = 0; i < ranges.Length(); i++) { michael@0: const SeekRange &r = ranges[i]; michael@0: if (r.mTimeStart < aTarget) { michael@0: so = r.mOffsetStart; michael@0: st = r.mTimeStart; michael@0: } michael@0: if (r.mTimeEnd >= aTarget && r.mTimeEnd < et) { michael@0: eo = r.mOffsetEnd; michael@0: et = r.mTimeEnd; michael@0: } michael@0: michael@0: if (r.mTimeStart < aTarget && aTarget <= r.mTimeEnd) { michael@0: // Target lies exactly in this range. michael@0: return ranges[i]; michael@0: } michael@0: } michael@0: if (aExact || eo == -1) { michael@0: return SeekRange(); michael@0: } michael@0: return SeekRange(so, eo, st, et); michael@0: } michael@0: michael@0: OggReader::IndexedSeekResult OggReader::RollbackIndexedSeek(int64_t aOffset) michael@0: { michael@0: mSkeletonState->Deactivate(); michael@0: MediaResource* resource = mDecoder->GetResource(); michael@0: NS_ENSURE_TRUE(resource != nullptr, SEEK_FATAL_ERROR); michael@0: nsresult res = resource->Seek(nsISeekableStream::NS_SEEK_SET, aOffset); michael@0: NS_ENSURE_SUCCESS(res, SEEK_FATAL_ERROR); michael@0: return SEEK_INDEX_FAIL; michael@0: } michael@0: michael@0: OggReader::IndexedSeekResult OggReader::SeekToKeyframeUsingIndex(int64_t aTarget) michael@0: { michael@0: MediaResource* resource = mDecoder->GetResource(); michael@0: NS_ENSURE_TRUE(resource != nullptr, SEEK_FATAL_ERROR); michael@0: if (!HasSkeleton() || !mSkeletonState->HasIndex()) { michael@0: return SEEK_INDEX_FAIL; michael@0: } michael@0: // We have an index from the Skeleton track, try to use it to seek. michael@0: nsAutoTArray tracks; michael@0: BuildSerialList(tracks); michael@0: SkeletonState::nsSeekTarget keyframe; michael@0: if (NS_FAILED(mSkeletonState->IndexedSeekTarget(aTarget, michael@0: tracks, michael@0: keyframe))) michael@0: { michael@0: // Could not locate a keypoint for the target in the index. michael@0: return SEEK_INDEX_FAIL; michael@0: } michael@0: michael@0: // Remember original resource read cursor position so we can rollback on failure. michael@0: int64_t tell = resource->Tell(); michael@0: michael@0: // Seek to the keypoint returned by the index. michael@0: if (keyframe.mKeyPoint.mOffset > resource->GetLength() || michael@0: keyframe.mKeyPoint.mOffset < 0) michael@0: { michael@0: // Index must be invalid. michael@0: return RollbackIndexedSeek(tell); michael@0: } michael@0: LOG(PR_LOG_DEBUG, ("Seeking using index to keyframe at offset %lld\n", michael@0: keyframe.mKeyPoint.mOffset)); michael@0: nsresult res = resource->Seek(nsISeekableStream::NS_SEEK_SET, michael@0: keyframe.mKeyPoint.mOffset); michael@0: NS_ENSURE_SUCCESS(res, SEEK_FATAL_ERROR); michael@0: michael@0: // We've moved the read set, so reset decode. michael@0: res = ResetDecode(); michael@0: NS_ENSURE_SUCCESS(res, SEEK_FATAL_ERROR); michael@0: michael@0: // Check that the page the index thinks is exactly here is actually exactly michael@0: // here. If not, the index is invalid. michael@0: ogg_page page; michael@0: int skippedBytes = 0; michael@0: PageSyncResult syncres = PageSync(resource, michael@0: &mOggState, michael@0: false, michael@0: keyframe.mKeyPoint.mOffset, michael@0: resource->GetLength(), michael@0: &page, michael@0: skippedBytes); michael@0: NS_ENSURE_TRUE(syncres != PAGE_SYNC_ERROR, SEEK_FATAL_ERROR); michael@0: if (syncres != PAGE_SYNC_OK || skippedBytes != 0) { michael@0: LOG(PR_LOG_DEBUG, ("Indexed-seek failure: Ogg Skeleton Index is invalid " michael@0: "or sync error after seek")); michael@0: return RollbackIndexedSeek(tell); michael@0: } michael@0: uint32_t serial = ogg_page_serialno(&page); michael@0: if (serial != keyframe.mSerial) { michael@0: // Serialno of page at offset isn't what the index told us to expect. michael@0: // Assume the index is invalid. michael@0: return RollbackIndexedSeek(tell); michael@0: } michael@0: OggCodecState* codecState = mCodecStore.Get(serial); michael@0: if (codecState && michael@0: codecState->mActive && michael@0: ogg_stream_pagein(&codecState->mState, &page) != 0) michael@0: { michael@0: // Couldn't insert page into the ogg resource, or somehow the resource michael@0: // is no longer active. michael@0: return RollbackIndexedSeek(tell); michael@0: } michael@0: return SEEK_OK; michael@0: } michael@0: michael@0: nsresult OggReader::SeekInBufferedRange(int64_t aTarget, michael@0: int64_t aAdjustedTarget, michael@0: int64_t aStartTime, michael@0: int64_t aEndTime, michael@0: const nsTArray& aRanges, michael@0: const SeekRange& aRange) michael@0: { michael@0: LOG(PR_LOG_DEBUG, ("%p Seeking in buffered data to %lld using bisection search", mDecoder, aTarget)); michael@0: nsresult res = NS_OK; michael@0: if (HasVideo() || aAdjustedTarget >= aTarget) { michael@0: // We know the exact byte range in which the target must lie. It must michael@0: // be buffered in the media cache. Seek there. michael@0: nsresult res = SeekBisection(aTarget, aRange, 0); michael@0: if (NS_FAILED(res) || !HasVideo()) { michael@0: return res; michael@0: } michael@0: michael@0: // We have an active Theora bitstream. Decode the next Theora frame, and michael@0: // extract its keyframe's time. michael@0: bool eof; michael@0: do { michael@0: bool skip = false; michael@0: eof = !DecodeVideoFrame(skip, 0); michael@0: { michael@0: ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); michael@0: if (mDecoder->IsShutdown()) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: } michael@0: } while (!eof && michael@0: mVideoQueue.GetSize() == 0); michael@0: michael@0: VideoData* video = mVideoQueue.PeekFront(); michael@0: if (video && !video->mKeyframe) { michael@0: // First decoded frame isn't a keyframe, seek back to previous keyframe, michael@0: // otherwise we'll get visual artifacts. michael@0: NS_ASSERTION(video->mTimecode != -1, "Must have a granulepos"); michael@0: int shift = mTheoraState->mInfo.keyframe_granule_shift; michael@0: int64_t keyframeGranulepos = (video->mTimecode >> shift) << shift; michael@0: int64_t keyframeTime = mTheoraState->StartTime(keyframeGranulepos); michael@0: SEEK_LOG(PR_LOG_DEBUG, ("Keyframe for %lld is at %lld, seeking back to it", michael@0: video->mTime, keyframeTime)); michael@0: aAdjustedTarget = std::min(aAdjustedTarget, keyframeTime); michael@0: } michael@0: } michael@0: if (aAdjustedTarget < aTarget) { michael@0: SeekRange k = SelectSeekRange(aRanges, michael@0: aAdjustedTarget, michael@0: aStartTime, michael@0: aEndTime, michael@0: false); michael@0: res = SeekBisection(aAdjustedTarget, k, SEEK_FUZZ_USECS); michael@0: } michael@0: return res; michael@0: } michael@0: michael@0: nsresult OggReader::SeekInUnbuffered(int64_t aTarget, michael@0: int64_t aStartTime, michael@0: int64_t aEndTime, michael@0: const nsTArray& aRanges) michael@0: { michael@0: LOG(PR_LOG_DEBUG, ("%p Seeking in unbuffered data to %lld using bisection search", mDecoder, aTarget)); michael@0: michael@0: // If we've got an active Theora bitstream, determine the maximum possible michael@0: // time in usecs which a keyframe could be before a given interframe. We michael@0: // subtract this from our seek target, seek to the new target, and then michael@0: // will decode forward to the original seek target. We should encounter a michael@0: // keyframe in that interval. This prevents us from needing to run two michael@0: // bisections; one for the seek target frame, and another to find its michael@0: // keyframe. It's usually faster to just download this extra data, rather michael@0: // tham perform two bisections to find the seek target's keyframe. We michael@0: // don't do this offsetting when seeking in a buffered range, michael@0: // as the extra decoding causes a noticeable speed hit when all the data michael@0: // is buffered (compared to just doing a bisection to exactly find the michael@0: // keyframe). michael@0: int64_t keyframeOffsetMs = 0; michael@0: if (HasVideo() && mTheoraState) { michael@0: keyframeOffsetMs = mTheoraState->MaxKeyframeOffset(); michael@0: } michael@0: #ifdef MOZ_OPUS michael@0: // Add in the Opus pre-roll if necessary, as well. michael@0: if (HasAudio() && mOpusState) { michael@0: keyframeOffsetMs = std::max(keyframeOffsetMs, SEEK_OPUS_PREROLL); michael@0: } michael@0: #endif /* MOZ_OPUS */ michael@0: int64_t seekTarget = std::max(aStartTime, aTarget - keyframeOffsetMs); michael@0: // Minimize the bisection search space using the known timestamps from the michael@0: // buffered ranges. michael@0: SeekRange k = SelectSeekRange(aRanges, seekTarget, aStartTime, aEndTime, false); michael@0: return SeekBisection(seekTarget, k, SEEK_FUZZ_USECS); michael@0: } michael@0: michael@0: nsresult OggReader::Seek(int64_t aTarget, michael@0: int64_t aStartTime, michael@0: int64_t aEndTime, michael@0: int64_t aCurrentTime) michael@0: { michael@0: NS_ASSERTION(mDecoder->OnDecodeThread(), "Should be on decode thread."); michael@0: if (mIsChained) michael@0: return NS_ERROR_FAILURE; michael@0: LOG(PR_LOG_DEBUG, ("%p About to seek to %lld", mDecoder, aTarget)); michael@0: nsresult res; michael@0: MediaResource* resource = mDecoder->GetResource(); michael@0: NS_ENSURE_TRUE(resource != nullptr, NS_ERROR_FAILURE); michael@0: int64_t adjustedTarget = aTarget; michael@0: #ifdef MOZ_OPUS michael@0: if (HasAudio() && mOpusState){ michael@0: adjustedTarget = std::max(aStartTime, aTarget - SEEK_OPUS_PREROLL); michael@0: } michael@0: #endif /* MOZ_OPUS */ michael@0: michael@0: if (adjustedTarget == aStartTime) { michael@0: // We've seeked to the media start. Just seek to the offset of the first michael@0: // content page. michael@0: res = resource->Seek(nsISeekableStream::NS_SEEK_SET, 0); michael@0: NS_ENSURE_SUCCESS(res,res); michael@0: michael@0: res = ResetDecode(true); michael@0: NS_ENSURE_SUCCESS(res,res); michael@0: michael@0: NS_ASSERTION(aStartTime != -1, "mStartTime should be known"); michael@0: { michael@0: ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); michael@0: mDecoder->UpdatePlaybackPosition(aStartTime); michael@0: } michael@0: } else { michael@0: // TODO: This may seek back unnecessarily far in the video, but we don't michael@0: // have a way of asking Skeleton to seek to a different target for each michael@0: // stream yet. Using adjustedTarget here is at least correct, if slow. michael@0: IndexedSeekResult sres = SeekToKeyframeUsingIndex(adjustedTarget); michael@0: NS_ENSURE_TRUE(sres != SEEK_FATAL_ERROR, NS_ERROR_FAILURE); michael@0: if (sres == SEEK_INDEX_FAIL) { michael@0: // No index or other non-fatal index-related failure. Try to seek michael@0: // using a bisection search. Determine the already downloaded data michael@0: // in the media cache, so we can try to seek in the cached data first. michael@0: nsAutoTArray ranges; michael@0: res = GetSeekRanges(ranges); michael@0: NS_ENSURE_SUCCESS(res,res); michael@0: michael@0: // Figure out if the seek target lies in a buffered range. michael@0: SeekRange r = SelectSeekRange(ranges, aTarget, aStartTime, aEndTime, true); michael@0: michael@0: if (!r.IsNull()) { michael@0: // We know the buffered range in which the seek target lies, do a michael@0: // bisection search in that buffered range. michael@0: res = SeekInBufferedRange(aTarget, adjustedTarget, aStartTime, aEndTime, ranges, r); michael@0: NS_ENSURE_SUCCESS(res,res); michael@0: } else { michael@0: // The target doesn't lie in a buffered range. Perform a bisection michael@0: // search over the whole media, using the known buffered ranges to michael@0: // reduce the search space. michael@0: res = SeekInUnbuffered(aTarget, aStartTime, aEndTime, ranges); michael@0: NS_ENSURE_SUCCESS(res,res); michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (HasVideo()) { michael@0: // Decode forwards until we find the next keyframe. This is required, michael@0: // as although the seek should finish on a page containing a keyframe, michael@0: // there may be non-keyframes in the page before the keyframe. michael@0: // When doing fastSeek we display the first frame after the seek, so michael@0: // we need to advance the decode to the keyframe otherwise we'll get michael@0: // visual artifacts in the first frame output after the seek. michael@0: bool skip = true; michael@0: while (DecodeVideoFrame(skip, 0) && skip) { michael@0: ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); michael@0: if (mDecoder->IsShutdown()) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: } michael@0: michael@0: #ifdef DEBUG michael@0: const VideoData* v = mVideoQueue.PeekFront(); michael@0: if (!v || !v->mKeyframe) { michael@0: NS_WARNING("Ogg seek didn't end up before a key frame!"); michael@0: } michael@0: #endif michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: // Reads a page from the media resource. michael@0: static PageSyncResult michael@0: PageSync(MediaResource* aResource, michael@0: ogg_sync_state* aState, michael@0: bool aCachedDataOnly, michael@0: int64_t aOffset, michael@0: int64_t aEndOffset, michael@0: ogg_page* aPage, michael@0: int& aSkippedBytes) michael@0: { michael@0: aSkippedBytes = 0; michael@0: // Sync to the next page. michael@0: int ret = 0; michael@0: uint32_t bytesRead = 0; michael@0: int64_t readHead = aOffset; michael@0: while (ret <= 0) { michael@0: ret = ogg_sync_pageseek(aState, aPage); michael@0: if (ret == 0) { michael@0: char* buffer = ogg_sync_buffer(aState, PAGE_STEP); michael@0: NS_ASSERTION(buffer, "Must have a buffer"); michael@0: michael@0: // Read from the file into the buffer michael@0: int64_t bytesToRead = std::min(static_cast(PAGE_STEP), michael@0: aEndOffset - readHead); michael@0: NS_ASSERTION(bytesToRead <= UINT32_MAX, "bytesToRead range check"); michael@0: if (bytesToRead <= 0) { michael@0: return PAGE_SYNC_END_OF_RANGE; michael@0: } michael@0: nsresult rv = NS_OK; michael@0: if (aCachedDataOnly) { michael@0: rv = aResource->ReadFromCache(buffer, readHead, michael@0: static_cast(bytesToRead)); michael@0: NS_ENSURE_SUCCESS(rv,PAGE_SYNC_ERROR); michael@0: bytesRead = static_cast(bytesToRead); michael@0: } else { michael@0: rv = aResource->Seek(nsISeekableStream::NS_SEEK_SET, readHead); michael@0: NS_ENSURE_SUCCESS(rv,PAGE_SYNC_ERROR); michael@0: rv = aResource->Read(buffer, michael@0: static_cast(bytesToRead), michael@0: &bytesRead); michael@0: NS_ENSURE_SUCCESS(rv,PAGE_SYNC_ERROR); michael@0: } michael@0: if (bytesRead == 0 && NS_SUCCEEDED(rv)) { michael@0: // End of file. michael@0: return PAGE_SYNC_END_OF_RANGE; michael@0: } michael@0: readHead += bytesRead; michael@0: michael@0: // Update the synchronisation layer with the number michael@0: // of bytes written to the buffer michael@0: ret = ogg_sync_wrote(aState, bytesRead); michael@0: NS_ENSURE_TRUE(ret == 0, PAGE_SYNC_ERROR); michael@0: continue; michael@0: } michael@0: michael@0: if (ret < 0) { michael@0: NS_ASSERTION(aSkippedBytes >= 0, "Offset >= 0"); michael@0: aSkippedBytes += -ret; michael@0: NS_ASSERTION(aSkippedBytes >= 0, "Offset >= 0"); michael@0: continue; michael@0: } michael@0: } michael@0: michael@0: return PAGE_SYNC_OK; michael@0: } michael@0: michael@0: nsresult OggReader::SeekBisection(int64_t aTarget, michael@0: const SeekRange& aRange, michael@0: uint32_t aFuzz) michael@0: { michael@0: NS_ASSERTION(mDecoder->OnDecodeThread(), "Should be on decode thread."); michael@0: nsresult res; michael@0: MediaResource* resource = mDecoder->GetResource(); michael@0: michael@0: if (aTarget == aRange.mTimeStart) { michael@0: if (NS_FAILED(ResetDecode())) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: res = resource->Seek(nsISeekableStream::NS_SEEK_SET, 0); michael@0: NS_ENSURE_SUCCESS(res,res); michael@0: return NS_OK; michael@0: } michael@0: michael@0: // Bisection search, find start offset of last page with end time less than michael@0: // the seek target. michael@0: ogg_int64_t startOffset = aRange.mOffsetStart; michael@0: ogg_int64_t startTime = aRange.mTimeStart; michael@0: ogg_int64_t startLength = 0; // Length of the page at startOffset. michael@0: ogg_int64_t endOffset = aRange.mOffsetEnd; michael@0: ogg_int64_t endTime = aRange.mTimeEnd; michael@0: michael@0: ogg_int64_t seekTarget = aTarget; michael@0: int64_t seekLowerBound = std::max(static_cast(0), aTarget - aFuzz); michael@0: int hops = 0; michael@0: DebugOnly previousGuess = -1; michael@0: int backsteps = 0; michael@0: const int maxBackStep = 10; michael@0: NS_ASSERTION(static_cast(PAGE_STEP) * pow(2.0, maxBackStep) < INT32_MAX, michael@0: "Backstep calculation must not overflow"); michael@0: michael@0: // Seek via bisection search. Loop until we find the offset where the page michael@0: // before the offset is before the seek target, and the page after the offset michael@0: // is after the seek target. michael@0: while (true) { michael@0: ogg_int64_t duration = 0; michael@0: double target = 0; michael@0: ogg_int64_t interval = 0; michael@0: ogg_int64_t guess = 0; michael@0: ogg_page page; michael@0: int skippedBytes = 0; michael@0: ogg_int64_t pageOffset = 0; michael@0: ogg_int64_t pageLength = 0; michael@0: ogg_int64_t granuleTime = -1; michael@0: bool mustBackoff = false; michael@0: michael@0: // Guess where we should bisect to, based on the bit rate and the time michael@0: // remaining in the interval. Loop until we can determine the time at michael@0: // the guess offset. michael@0: while (true) { michael@0: michael@0: // Discard any previously buffered packets/pages. michael@0: if (NS_FAILED(ResetDecode())) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: interval = endOffset - startOffset - startLength; michael@0: if (interval == 0) { michael@0: // Our interval is empty, we've found the optimal seek point, as the michael@0: // page at the start offset is before the seek target, and the page michael@0: // at the end offset is after the seek target. michael@0: SEEK_LOG(PR_LOG_DEBUG, ("Interval narrowed, terminating bisection.")); michael@0: break; michael@0: } michael@0: michael@0: // Guess bisection point. michael@0: duration = endTime - startTime; michael@0: target = (double)(seekTarget - startTime) / (double)duration; michael@0: guess = startOffset + startLength + michael@0: static_cast((double)interval * target); michael@0: guess = std::min(guess, endOffset - PAGE_STEP); michael@0: if (mustBackoff) { michael@0: // We previously failed to determine the time at the guess offset, michael@0: // probably because we ran out of data to decode. This usually happens michael@0: // when we guess very close to the end offset. So reduce the guess michael@0: // offset using an exponential backoff until we determine the time. michael@0: SEEK_LOG(PR_LOG_DEBUG, ("Backing off %d bytes, backsteps=%d", michael@0: static_cast(PAGE_STEP * pow(2.0, backsteps)), backsteps)); michael@0: guess -= PAGE_STEP * static_cast(pow(2.0, backsteps)); michael@0: michael@0: if (guess <= startOffset) { michael@0: // We've tried to backoff to before the start offset of our seek michael@0: // range. This means we couldn't find a seek termination position michael@0: // near the end of the seek range, so just set the seek termination michael@0: // condition, and break out of the bisection loop. We'll begin michael@0: // decoding from the start of the seek range. michael@0: interval = 0; michael@0: break; michael@0: } michael@0: michael@0: backsteps = std::min(backsteps + 1, maxBackStep); michael@0: // We reset mustBackoff. If we still need to backoff further, it will michael@0: // be set to true again. michael@0: mustBackoff = false; michael@0: } else { michael@0: backsteps = 0; michael@0: } michael@0: guess = std::max(guess, startOffset + startLength); michael@0: michael@0: SEEK_LOG(PR_LOG_DEBUG, ("Seek loop start[o=%lld..%lld t=%lld] " michael@0: "end[o=%lld t=%lld] " michael@0: "interval=%lld target=%lf guess=%lld", michael@0: startOffset, (startOffset+startLength), startTime, michael@0: endOffset, endTime, interval, target, guess)); michael@0: michael@0: NS_ASSERTION(guess >= startOffset + startLength, "Guess must be after range start"); michael@0: NS_ASSERTION(guess < endOffset, "Guess must be before range end"); michael@0: NS_ASSERTION(guess != previousGuess, "Guess should be different to previous"); michael@0: previousGuess = guess; michael@0: michael@0: hops++; michael@0: michael@0: // Locate the next page after our seek guess, and then figure out the michael@0: // granule time of the audio and video bitstreams there. We can then michael@0: // make a bisection decision based on our location in the media. michael@0: PageSyncResult res = PageSync(resource, michael@0: &mOggState, michael@0: false, michael@0: guess, michael@0: endOffset, michael@0: &page, michael@0: skippedBytes); michael@0: NS_ENSURE_TRUE(res != PAGE_SYNC_ERROR, NS_ERROR_FAILURE); michael@0: michael@0: if (res == PAGE_SYNC_END_OF_RANGE) { michael@0: // Our guess was too close to the end, we've ended up reading the end michael@0: // page. Backoff exponentially from the end point, in case the last michael@0: // page/frame/sample is huge. michael@0: mustBackoff = true; michael@0: SEEK_LOG(PR_LOG_DEBUG, ("Hit the end of range, backing off")); michael@0: continue; michael@0: } michael@0: michael@0: // We've located a page of length |ret| at |guess + skippedBytes|. michael@0: // Remember where the page is located. michael@0: pageOffset = guess + skippedBytes; michael@0: pageLength = page.header_len + page.body_len; michael@0: michael@0: // Read pages until we can determine the granule time of the audio and michael@0: // video bitstream. michael@0: ogg_int64_t audioTime = -1; michael@0: ogg_int64_t videoTime = -1; michael@0: do { michael@0: // Add the page to its codec state, determine its granule time. michael@0: uint32_t serial = ogg_page_serialno(&page); michael@0: OggCodecState* codecState = mCodecStore.Get(serial); michael@0: if (codecState && codecState->mActive) { michael@0: int ret = ogg_stream_pagein(&codecState->mState, &page); michael@0: NS_ENSURE_TRUE(ret == 0, NS_ERROR_FAILURE); michael@0: } michael@0: michael@0: ogg_int64_t granulepos = ogg_page_granulepos(&page); michael@0: michael@0: if (HasAudio() && granulepos > 0 && audioTime == -1) { michael@0: if (mVorbisState && serial == mVorbisState->mSerial) { michael@0: audioTime = mVorbisState->Time(granulepos); michael@0: #ifdef MOZ_OPUS michael@0: } else if (mOpusState && serial == mOpusState->mSerial) { michael@0: audioTime = mOpusState->Time(granulepos); michael@0: #endif michael@0: } michael@0: } michael@0: michael@0: if (HasVideo() && michael@0: granulepos > 0 && michael@0: serial == mTheoraState->mSerial && michael@0: videoTime == -1) { michael@0: videoTime = mTheoraState->Time(granulepos); michael@0: } michael@0: michael@0: if (pageOffset + pageLength >= endOffset) { michael@0: // Hit end of readable data. michael@0: break; michael@0: } michael@0: michael@0: if (!ReadOggPage(&page)) { michael@0: break; michael@0: } michael@0: michael@0: } while ((HasAudio() && audioTime == -1) || michael@0: (HasVideo() && videoTime == -1)); michael@0: michael@0: michael@0: if ((HasAudio() && audioTime == -1) || michael@0: (HasVideo() && videoTime == -1)) michael@0: { michael@0: // We don't have timestamps for all active tracks... michael@0: if (pageOffset == startOffset + startLength && michael@0: pageOffset + pageLength >= endOffset) { michael@0: // We read the entire interval without finding timestamps for all michael@0: // active tracks. We know the interval start offset is before the seek michael@0: // target, and the interval end is after the seek target, and we can't michael@0: // terminate inside the interval, so we terminate the seek at the michael@0: // start of the interval. michael@0: interval = 0; michael@0: break; michael@0: } michael@0: michael@0: // We should backoff; cause the guess to back off from the end, so michael@0: // that we've got more room to capture. michael@0: mustBackoff = true; michael@0: continue; michael@0: } michael@0: michael@0: // We've found appropriate time stamps here. Proceed to bisect michael@0: // the search space. michael@0: granuleTime = std::max(audioTime, videoTime); michael@0: NS_ASSERTION(granuleTime > 0, "Must get a granuletime"); michael@0: break; michael@0: } // End of "until we determine time at guess offset" loop. michael@0: michael@0: if (interval == 0) { michael@0: // Seek termination condition; we've found the page boundary of the michael@0: // last page before the target, and the first page after the target. michael@0: SEEK_LOG(PR_LOG_DEBUG, ("Terminating seek at offset=%lld", startOffset)); michael@0: NS_ASSERTION(startTime < aTarget, "Start time must always be less than target"); michael@0: res = resource->Seek(nsISeekableStream::NS_SEEK_SET, startOffset); michael@0: NS_ENSURE_SUCCESS(res,res); michael@0: if (NS_FAILED(ResetDecode())) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: break; michael@0: } michael@0: michael@0: SEEK_LOG(PR_LOG_DEBUG, ("Time at offset %lld is %lld", guess, granuleTime)); michael@0: if (granuleTime < seekTarget && granuleTime > seekLowerBound) { michael@0: // We're within the fuzzy region in which we want to terminate the search. michael@0: res = resource->Seek(nsISeekableStream::NS_SEEK_SET, pageOffset); michael@0: NS_ENSURE_SUCCESS(res,res); michael@0: if (NS_FAILED(ResetDecode())) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: SEEK_LOG(PR_LOG_DEBUG, ("Terminating seek at offset=%lld", pageOffset)); michael@0: break; michael@0: } michael@0: michael@0: if (granuleTime >= seekTarget) { michael@0: // We've landed after the seek target. michael@0: NS_ASSERTION(pageOffset < endOffset, "offset_end must decrease"); michael@0: endOffset = pageOffset; michael@0: endTime = granuleTime; michael@0: } else if (granuleTime < seekTarget) { michael@0: // Landed before seek target. michael@0: NS_ASSERTION(pageOffset >= startOffset + startLength, michael@0: "Bisection point should be at or after end of first page in interval"); michael@0: startOffset = pageOffset; michael@0: startLength = pageLength; michael@0: startTime = granuleTime; michael@0: } michael@0: NS_ASSERTION(startTime < seekTarget, "Must be before seek target"); michael@0: NS_ASSERTION(endTime >= seekTarget, "End must be after seek target"); michael@0: } michael@0: michael@0: SEEK_LOG(PR_LOG_DEBUG, ("Seek complete in %d bisections.", hops)); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult OggReader::GetBuffered(dom::TimeRanges* aBuffered, int64_t aStartTime) michael@0: { michael@0: { michael@0: mozilla::ReentrantMonitorAutoEnter mon(mMonitor); michael@0: if (mIsChained) michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: #ifdef OGG_ESTIMATE_BUFFERED michael@0: return MediaDecoderReader::GetBuffered(aBuffered, aStartTime); michael@0: #else michael@0: // HasAudio and HasVideo are not used here as they take a lock and cause michael@0: // a deadlock. Accessing mInfo doesn't require a lock - it doesn't change michael@0: // after metadata is read. michael@0: if (!mInfo.HasValidMedia()) { michael@0: // No need to search through the file if there are no audio or video tracks michael@0: return NS_OK; michael@0: } michael@0: michael@0: MediaResource* resource = mDecoder->GetResource(); michael@0: nsTArray ranges; michael@0: nsresult res = resource->GetCachedRanges(ranges); michael@0: NS_ENSURE_SUCCESS(res, res); michael@0: michael@0: // Traverse across the buffered byte ranges, determining the time ranges michael@0: // they contain. MediaResource::GetNextCachedData(offset) returns -1 when michael@0: // offset is after the end of the media resource, or there's no more cached michael@0: // data after the offset. This loop will run until we've checked every michael@0: // buffered range in the media, in increasing order of offset. michael@0: nsAutoOggSyncState sync; michael@0: for (uint32_t index = 0; index < ranges.Length(); index++) { michael@0: // Ensure the offsets are after the header pages. michael@0: int64_t startOffset = ranges[index].mStart; michael@0: int64_t endOffset = ranges[index].mEnd; michael@0: michael@0: // Because the granulepos time is actually the end time of the page, michael@0: // we special-case (startOffset == 0) so that the first michael@0: // buffered range always appears to be buffered from the media start michael@0: // time, rather than from the end-time of the first page. michael@0: int64_t startTime = (startOffset == 0) ? aStartTime : -1; michael@0: michael@0: // Find the start time of the range. Read pages until we find one with a michael@0: // granulepos which we can convert into a timestamp to use as the time of michael@0: // the start of the buffered range. michael@0: ogg_sync_reset(&sync.mState); michael@0: while (startTime == -1) { michael@0: ogg_page page; michael@0: int32_t discard; michael@0: PageSyncResult res = PageSync(resource, michael@0: &sync.mState, michael@0: true, michael@0: startOffset, michael@0: endOffset, michael@0: &page, michael@0: discard); michael@0: if (res == PAGE_SYNC_ERROR) { michael@0: return NS_ERROR_FAILURE; michael@0: } else if (res == PAGE_SYNC_END_OF_RANGE) { michael@0: // Hit the end of range without reading a page, give up trying to michael@0: // find a start time for this buffered range, skip onto the next one. michael@0: break; michael@0: } michael@0: michael@0: int64_t granulepos = ogg_page_granulepos(&page); michael@0: if (granulepos == -1) { michael@0: // Page doesn't have an end time, advance to the next page michael@0: // until we find one. michael@0: startOffset += page.header_len + page.body_len; michael@0: continue; michael@0: } michael@0: michael@0: uint32_t serial = ogg_page_serialno(&page); michael@0: if (mVorbisState && serial == mVorbisSerial) { michael@0: startTime = VorbisState::Time(&mVorbisInfo, granulepos); michael@0: NS_ASSERTION(startTime > 0, "Must have positive start time"); michael@0: } michael@0: #ifdef MOZ_OPUS michael@0: else if (mOpusState && serial == mOpusSerial) { michael@0: startTime = OpusState::Time(mOpusPreSkip, granulepos); michael@0: NS_ASSERTION(startTime > 0, "Must have positive start time"); michael@0: } michael@0: #endif /* MOZ_OPUS */ michael@0: else if (mTheoraState && serial == mTheoraSerial) { michael@0: startTime = TheoraState::Time(&mTheoraInfo, granulepos); michael@0: NS_ASSERTION(startTime > 0, "Must have positive start time"); michael@0: } michael@0: else if (mCodecStore.Contains(serial)) { michael@0: // Stream is not the theora or vorbis stream we're playing, michael@0: // but is one that we have header data for. michael@0: startOffset += page.header_len + page.body_len; michael@0: continue; michael@0: } michael@0: else { michael@0: // Page is for a stream we don't know about (possibly a chained michael@0: // ogg), return OK to abort the finding any further ranges. This michael@0: // prevents us searching through the rest of the media when we michael@0: // may not be able to extract timestamps from it. michael@0: SetChained(true); michael@0: return NS_OK; michael@0: } michael@0: } michael@0: michael@0: if (startTime != -1) { michael@0: // We were able to find a start time for that range, see if we can michael@0: // find an end time. michael@0: int64_t endTime = RangeEndTime(startOffset, endOffset, true); michael@0: if (endTime != -1) { michael@0: aBuffered->Add((startTime - aStartTime) / static_cast(USECS_PER_S), michael@0: (endTime - aStartTime) / static_cast(USECS_PER_S)); michael@0: } michael@0: } michael@0: } michael@0: michael@0: return NS_OK; michael@0: #endif michael@0: } michael@0: michael@0: OggCodecStore::OggCodecStore() michael@0: : mMonitor("CodecStore") michael@0: { michael@0: } michael@0: michael@0: void OggCodecStore::Add(uint32_t serial, OggCodecState* codecState) michael@0: { michael@0: MonitorAutoLock mon(mMonitor); michael@0: mCodecStates.Put(serial, codecState); michael@0: } michael@0: michael@0: bool OggCodecStore::Contains(uint32_t serial) michael@0: { michael@0: MonitorAutoLock mon(mMonitor); michael@0: return mCodecStates.Get(serial, nullptr); michael@0: } michael@0: michael@0: OggCodecState* OggCodecStore::Get(uint32_t serial) michael@0: { michael@0: MonitorAutoLock mon(mMonitor); michael@0: return mCodecStates.Get(serial); michael@0: } michael@0: michael@0: } // namespace mozilla michael@0: