diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /dom/media/ogg/OggDemuxer.cpp | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream/115.8.0esr.tar.xz firefox-esr-upstream/115.8.0esr.zip |
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/media/ogg/OggDemuxer.cpp')
-rw-r--r-- | dom/media/ogg/OggDemuxer.cpp | 2172 |
1 files changed, 2172 insertions, 0 deletions
diff --git a/dom/media/ogg/OggDemuxer.cpp b/dom/media/ogg/OggDemuxer.cpp new file mode 100644 index 0000000000..2d1fdd3097 --- /dev/null +++ b/dom/media/ogg/OggDemuxer.cpp @@ -0,0 +1,2172 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "OggDemuxer.h" +#include "OggRLBox.h" +#include "MediaDataDemuxer.h" +#include "OggCodecState.h" +#include "XiphExtradata.h" +#include "mozilla/AbstractThread.h" +#include "mozilla/Atomics.h" +#include "mozilla/PodOperations.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/SharedThreadPool.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TimeStamp.h" +#ifdef MOZ_WASM_SANDBOXING_OGG +# include "mozilla/ipc/LibrarySandboxPreload.h" +#endif +#include "nsAutoRef.h" +#include "nsError.h" + +#include <algorithm> + +extern mozilla::LazyLogModule gMediaDemuxerLog; +#define OGG_DEBUG(arg, ...) \ + DDMOZ_LOG(gMediaDemuxerLog, mozilla::LogLevel::Debug, "::%s: " arg, \ + __func__, ##__VA_ARGS__) + +// Un-comment to enable logging of seek bisections. +// #define SEEK_LOGGING +#ifdef SEEK_LOGGING +# define SEEK_LOG(type, msg) MOZ_LOG(gMediaDemuxerLog, type, msg) +#else +# define SEEK_LOG(type, msg) +#endif + +#define CopyAndVerifyOrFail(t, cond, failed) \ + (t).copy_and_verify([&](auto val) { \ + if (!(cond)) { \ + *(failed) = true; \ + } \ + return val; \ + }) + +namespace mozilla { + +using media::TimeInterval; +using media::TimeIntervals; +using media::TimeUnit; + +// The number of microseconds of "fuzz" we use in a bisection search over +// HTTP. When we're seeking with fuzz, we'll stop the search if a bisection +// lands between the seek target and OGG_SEEK_FUZZ_USECS microseconds before the +// seek target. This is becaue it's usually quicker to just keep downloading +// from an exisiting connection than to do another bisection inside that +// small range, which would open a new HTTP connetion. +static const uint32_t OGG_SEEK_FUZZ_USECS = 500000; + +// The number of microseconds of "pre-roll" we use for Opus streams. +// The specification recommends 80 ms. +static const TimeUnit OGG_SEEK_OPUS_PREROLL = TimeUnit::FromMicroseconds(80000); + +static Atomic<uint32_t> sStreamSourceID(0u); + +OggDemuxer::nsAutoOggSyncState::nsAutoOggSyncState(rlbox_sandbox_ogg* aSandbox) + : mSandbox(aSandbox) { + if (mSandbox) { + tainted_ogg<ogg_sync_state*> state = + mSandbox->malloc_in_sandbox<ogg_sync_state>(); + MOZ_RELEASE_ASSERT(state != nullptr); + mState = state.to_opaque(); + sandbox_invoke(*mSandbox, ogg_sync_init, mState); + } +} +OggDemuxer::nsAutoOggSyncState::~nsAutoOggSyncState() { + if (mSandbox) { + sandbox_invoke(*mSandbox, ogg_sync_clear, mState); + mSandbox->free_in_sandbox(rlbox::from_opaque(mState)); + tainted_ogg<ogg_sync_state*> null = nullptr; + mState = null.to_opaque(); + } +} + +/* static */ +rlbox_sandbox_ogg* OggDemuxer::CreateSandbox() { + rlbox_sandbox_ogg* sandbox = new rlbox_sandbox_ogg(); +#ifdef MOZ_WASM_SANDBOXING_OGG + bool success = sandbox->create_sandbox(false /* infallible */); +#else + bool success = sandbox->create_sandbox(); +#endif + if (!success) { + delete sandbox; + sandbox = nullptr; + } + return sandbox; +} + +void OggDemuxer::SandboxDestroy::operator()(rlbox_sandbox_ogg* sandbox) { + if (sandbox) { + sandbox->destroy_sandbox(); + delete sandbox; + } +} + +// Return the corresponding category in aKind based on the following specs. +// (https://www.whatwg.org/specs/web-apps/current- +// work/multipage/embedded-content.html#dom-audiotrack-kind) & +// (http://wiki.xiph.org/SkeletonHeaders) +const nsString OggDemuxer::GetKind(const nsCString& aRole) { + if (aRole.Find("audio/main") != -1 || aRole.Find("video/main") != -1) { + return u"main"_ns; + } else if (aRole.Find("audio/alternate") != -1 || + aRole.Find("video/alternate") != -1) { + return u"alternative"_ns; + } else if (aRole.Find("audio/audiodesc") != -1) { + return u"descriptions"_ns; + } else if (aRole.Find("audio/described") != -1) { + return u"main-desc"_ns; + } else if (aRole.Find("audio/dub") != -1) { + return u"translation"_ns; + } else if (aRole.Find("audio/commentary") != -1) { + return u"commentary"_ns; + } else if (aRole.Find("video/sign") != -1) { + return u"sign"_ns; + } else if (aRole.Find("video/captioned") != -1) { + return u"captions"_ns; + } else if (aRole.Find("video/subtitled") != -1) { + return u"subtitles"_ns; + } + return u""_ns; +} + +void OggDemuxer::InitTrack(MessageField* aMsgInfo, TrackInfo* aInfo, + bool aEnable) { + MOZ_ASSERT(aMsgInfo); + MOZ_ASSERT(aInfo); + + nsCString* sName = aMsgInfo->mValuesStore.Get(eName); + nsCString* sRole = aMsgInfo->mValuesStore.Get(eRole); + nsCString* sTitle = aMsgInfo->mValuesStore.Get(eTitle); + nsCString* sLanguage = aMsgInfo->mValuesStore.Get(eLanguage); + aInfo->Init(sName ? NS_ConvertUTF8toUTF16(*sName) : EmptyString(), + sRole ? GetKind(*sRole) : u""_ns, + sTitle ? NS_ConvertUTF8toUTF16(*sTitle) : EmptyString(), + sLanguage ? NS_ConvertUTF8toUTF16(*sLanguage) : EmptyString(), + aEnable); +} + +OggDemuxer::OggDemuxer(MediaResource* aResource) + : mSandbox(CreateSandbox()), + mTheoraState(nullptr), + mVorbisState(nullptr), + mOpusState(nullptr), + mFlacState(nullptr), + mOpusEnabled(MediaDecoder::IsOpusEnabled()), + mSkeletonState(nullptr), + mAudioOggState(aResource, mSandbox.get()), + mVideoOggState(aResource, mSandbox.get()), + mIsChained(false), + mTimedMetadataEvent(nullptr), + mOnSeekableEvent(nullptr) { + MOZ_COUNT_CTOR(OggDemuxer); + // aResource is referenced through inner m{Audio,Video}OffState members. + DDLINKCHILD("resource", aResource); +} + +OggDemuxer::~OggDemuxer() { + MOZ_COUNT_DTOR(OggDemuxer); + Reset(TrackInfo::kAudioTrack); + Reset(TrackInfo::kVideoTrack); +} + +void OggDemuxer::SetChainingEvents(TimedMetadataEventProducer* aMetadataEvent, + MediaEventProducer<void>* aOnSeekableEvent) { + mTimedMetadataEvent = aMetadataEvent; + mOnSeekableEvent = aOnSeekableEvent; +} + +bool OggDemuxer::HasAudio() const { + return mVorbisState || mOpusState || mFlacState; +} + +bool OggDemuxer::HasVideo() const { return mTheoraState; } + +bool OggDemuxer::HaveStartTime() const { return mStartTime.isSome(); } + +int64_t OggDemuxer::StartTime() const { return mStartTime.refOr(0); } + +bool OggDemuxer::HaveStartTime(TrackInfo::TrackType aType) { + return OggState(aType).mStartTime.isSome(); +} + +int64_t OggDemuxer::StartTime(TrackInfo::TrackType aType) { + return OggState(aType).mStartTime.refOr(TimeUnit::Zero()).ToMicroseconds(); +} + +RefPtr<OggDemuxer::InitPromise> OggDemuxer::Init() { + if (!mSandbox) { + return InitPromise::CreateAndReject(NS_ERROR_OUT_OF_MEMORY, __func__); + } + const char RLBOX_OGG_RETURN_CODE_SAFE[] = + "Return codes only control whether to early exit. Incorrect return codes " + "will not lead to memory safety issues in the renderer."; + + int ret = sandbox_invoke(*mSandbox, ogg_sync_init, + OggSyncState(TrackInfo::kAudioTrack)) + .unverified_safe_because(RLBOX_OGG_RETURN_CODE_SAFE); + if (ret != 0) { + return InitPromise::CreateAndReject(NS_ERROR_OUT_OF_MEMORY, __func__); + } + ret = sandbox_invoke(*mSandbox, ogg_sync_init, + OggSyncState(TrackInfo::kVideoTrack)) + .unverified_safe_because(RLBOX_OGG_RETURN_CODE_SAFE); + if (ret != 0) { + return InitPromise::CreateAndReject(NS_ERROR_OUT_OF_MEMORY, __func__); + } + if (ReadMetadata() != NS_OK) { + return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_METADATA_ERR, + __func__); + } + + if (!GetNumberTracks(TrackInfo::kAudioTrack) && + !GetNumberTracks(TrackInfo::kVideoTrack)) { + return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_METADATA_ERR, + __func__); + } + + return InitPromise::CreateAndResolve(NS_OK, __func__); +} + +OggCodecState* OggDemuxer::GetTrackCodecState( + TrackInfo::TrackType aType) const { + switch (aType) { + case TrackInfo::kAudioTrack: + if (mVorbisState) { + return mVorbisState; + } else if (mOpusState) { + return mOpusState; + } else { + return mFlacState; + } + case TrackInfo::kVideoTrack: + return mTheoraState; + default: + return 0; + } +} + +TrackInfo::TrackType OggDemuxer::GetCodecStateType( + OggCodecState* aState) const { + switch (aState->GetType()) { + case OggCodecState::TYPE_THEORA: + return TrackInfo::kVideoTrack; + case OggCodecState::TYPE_OPUS: + case OggCodecState::TYPE_VORBIS: + case OggCodecState::TYPE_FLAC: + return TrackInfo::kAudioTrack; + default: + return TrackInfo::kUndefinedTrack; + } +} + +uint32_t OggDemuxer::GetNumberTracks(TrackInfo::TrackType aType) const { + switch (aType) { + case TrackInfo::kAudioTrack: + return HasAudio() ? 1 : 0; + case TrackInfo::kVideoTrack: + return HasVideo() ? 1 : 0; + default: + return 0; + } +} + +UniquePtr<TrackInfo> OggDemuxer::GetTrackInfo(TrackInfo::TrackType aType, + size_t aTrackNumber) const { + switch (aType) { + case TrackInfo::kAudioTrack: + return mInfo.mAudio.Clone(); + case TrackInfo::kVideoTrack: + return mInfo.mVideo.Clone(); + default: + return nullptr; + } +} + +already_AddRefed<MediaTrackDemuxer> OggDemuxer::GetTrackDemuxer( + TrackInfo::TrackType aType, uint32_t aTrackNumber) { + if (GetNumberTracks(aType) <= aTrackNumber) { + return nullptr; + } + RefPtr<OggTrackDemuxer> e = new OggTrackDemuxer(this, aType, aTrackNumber); + DDLINKCHILD("track demuxer", e.get()); + mDemuxers.AppendElement(e); + + return e.forget(); +} + +nsresult OggDemuxer::Reset(TrackInfo::TrackType aType) { + // Discard any previously buffered packets/pages. + if (mSandbox) { + sandbox_invoke(*mSandbox, ogg_sync_reset, OggSyncState(aType)); + } + OggCodecState* trackState = GetTrackCodecState(aType); + if (trackState) { + return trackState->Reset(); + } + OggState(aType).mNeedKeyframe = true; + return NS_OK; +} + +bool OggDemuxer::ReadHeaders(TrackInfo::TrackType aType, + OggCodecState* aState) { + while (!aState->DoneReadingHeaders()) { + DemuxUntilPacketAvailable(aType, aState); + OggPacketPtr packet = aState->PacketOut(); + if (!packet) { + OGG_DEBUG("Ran out of header packets early; deactivating stream %" PRIu32, + aState->mSerial); + aState->Deactivate(); + return false; + } + + // Local OggCodecState needs to decode headers in order to process + // packet granulepos -> time mappings, etc. + if (!aState->DecodeHeader(std::move(packet))) { + OGG_DEBUG( + "Failed to decode ogg header packet; deactivating stream %" PRIu32, + aState->mSerial); + aState->Deactivate(); + return false; + } + } + + return aState->Init(); +} + +void OggDemuxer::BuildSerialList(nsTArray<uint32_t>& aTracks) { + // Obtaining seek index information for currently active bitstreams. + if (HasVideo()) { + aTracks.AppendElement(mTheoraState->mSerial); + } + if (HasAudio()) { + if (mVorbisState) { + aTracks.AppendElement(mVorbisState->mSerial); + } else if (mOpusState) { + aTracks.AppendElement(mOpusState->mSerial); + } + } +} + +void OggDemuxer::SetupTarget(OggCodecState** aSavedState, + OggCodecState* aNewState) { + if (*aSavedState) { + (*aSavedState)->Reset(); + } + + if (aNewState->GetInfo()->GetAsAudioInfo()) { + mInfo.mAudio = *aNewState->GetInfo()->GetAsAudioInfo(); + } else { + mInfo.mVideo = *aNewState->GetInfo()->GetAsVideoInfo(); + } + *aSavedState = aNewState; +} + +void OggDemuxer::SetupTargetSkeleton() { + // Setup skeleton related information after mVorbisState & mTheroState + // being set (if they exist). + if (mSkeletonState) { + if (!HasAudio() && !HasVideo()) { + // We have a skeleton track, but no audio or video, may as well disable + // the skeleton, we can't do anything useful with this media. + OGG_DEBUG("Deactivating skeleton stream %" PRIu32, + mSkeletonState->mSerial); + mSkeletonState->Deactivate(); + } else if (ReadHeaders(TrackInfo::kAudioTrack, mSkeletonState) && + mSkeletonState->HasIndex()) { + // We don't particularly care about which track we are currently using + // as both MediaResource points to the same content. + // Extract the duration info out of the index, so we don't need to seek to + // the end of resource to get it. + nsTArray<uint32_t> tracks; + BuildSerialList(tracks); + int64_t duration = 0; + if (NS_SUCCEEDED(mSkeletonState->GetDuration(tracks, duration))) { + OGG_DEBUG("Got duration from Skeleton index %" PRId64, duration); + mInfo.mMetadataDuration.emplace(TimeUnit::FromMicroseconds(duration)); + } + } + } +} + +void OggDemuxer::SetupMediaTracksInfo(const nsTArray<uint32_t>& aSerials) { + // For each serial number + // 1. Retrieve a codecState from mCodecStore by this serial number. + // 2. Retrieve a message field from mMsgFieldStore by this serial number. + // 3. For now, skip if the serial number refers to a non-primary bitstream. + // 4. Setup track and other audio/video related information per different + // types. + for (size_t i = 0; i < aSerials.Length(); i++) { + uint32_t serial = aSerials[i]; + OggCodecState* codecState = mCodecStore.Get(serial); + + MessageField* msgInfo = nullptr; + if (mSkeletonState) { + mSkeletonState->mMsgFieldStore.Get(serial, &msgInfo); + } + + OggCodecState* primeState = nullptr; + switch (codecState->GetType()) { + case OggCodecState::TYPE_THEORA: + primeState = mTheoraState; + break; + case OggCodecState::TYPE_VORBIS: + primeState = mVorbisState; + break; + case OggCodecState::TYPE_OPUS: + primeState = mOpusState; + break; + case OggCodecState::TYPE_FLAC: + primeState = mFlacState; + break; + default: + break; + } + if (primeState && primeState == codecState) { + bool isAudio = primeState->GetInfo()->GetAsAudioInfo(); + if (msgInfo) { + InitTrack( + msgInfo, + isAudio ? static_cast<TrackInfo*>(&mInfo.mAudio) : &mInfo.mVideo, + true); + } + FillTags(isAudio ? static_cast<TrackInfo*>(&mInfo.mAudio) : &mInfo.mVideo, + primeState->GetTags()); + } + } +} + +void OggDemuxer::FillTags(TrackInfo* aInfo, UniquePtr<MetadataTags>&& aTags) { + if (!aTags) { + return; + } + UniquePtr<MetadataTags> tags(std::move(aTags)); + for (const auto& entry : *tags) { + aInfo->mTags.AppendElement(MetadataTag(entry.GetKey(), entry.GetData())); + } +} + +nsresult OggDemuxer::ReadMetadata() { + OGG_DEBUG("OggDemuxer::ReadMetadata called!"); + + // We read packets until all bitstreams have read all their header packets. + // We record the offset of the first non-header page so that we know + // what page to seek to when seeking to the media start. + + // @FIXME we have to read all the header packets on all the streams + // and THEN we can run SetupTarget* + // @fixme fixme + + TrackInfo::TrackType tracks[2] = {TrackInfo::kAudioTrack, + TrackInfo::kVideoTrack}; + + nsTArray<OggCodecState*> bitstreams; + nsTArray<uint32_t> serials; + + for (uint32_t i = 0; i < ArrayLength(tracks); i++) { + tainted_ogg<ogg_page*> page = mSandbox->malloc_in_sandbox<ogg_page>(); + if (!page) { + return NS_ERROR_OUT_OF_MEMORY; + } + auto clean_page = MakeScopeExit([&] { mSandbox->free_in_sandbox(page); }); + + bool readAllBOS = false; + while (!readAllBOS) { + if (!ReadOggPage(tracks[i], page.to_opaque())) { + // Some kind of error... + OGG_DEBUG("OggDemuxer::ReadOggPage failed? leaving ReadMetadata..."); + return NS_ERROR_FAILURE; + } + + uint32_t serial = static_cast<uint32_t>( + sandbox_invoke(*mSandbox, ogg_page_serialno, page) + .unverified_safe_because(RLBOX_OGG_PAGE_SERIAL_REASON)); + + if (!sandbox_invoke(*mSandbox, ogg_page_bos, page) + .unverified_safe_because( + "If this value is incorrect, it would mean not all " + "bitstreams are read. This does not affect the memory " + "safety of the renderer.")) { + // We've encountered a non Beginning Of Stream page. No more BOS pages + // can follow in this Ogg segment, so there will be no other bitstreams + // in the Ogg (unless it's invalid). + readAllBOS = true; + } else if (!mCodecStore.Contains(serial)) { + // We've not encountered a stream with this serial number before. Create + // an OggCodecState to demux it, and map that to the OggCodecState + // in mCodecStates. + OggCodecState* const codecState = mCodecStore.Add( + serial, + OggCodecState::Create(mSandbox.get(), page.to_opaque(), serial)); + bitstreams.AppendElement(codecState); + serials.AppendElement(serial); + } + if (NS_FAILED(DemuxOggPage(tracks[i], page.to_opaque()))) { + return NS_ERROR_FAILURE; + } + } + } + + // We've read all BOS pages, so we know the streams contained in the media. + // 1. Find the first encountered Theora/Vorbis/Opus bitstream, and configure + // it as the target A/V bitstream. + // 2. Deactivate the rest of bitstreams for now, until we have MediaInfo + // support multiple track infos. + for (uint32_t i = 0; i < bitstreams.Length(); ++i) { + OggCodecState* s = bitstreams[i]; + if (s) { + if (s->GetType() == OggCodecState::TYPE_THEORA && + ReadHeaders(TrackInfo::kVideoTrack, s)) { + if (!mTheoraState) { + SetupTarget(&mTheoraState, s); + } else { + s->Deactivate(); + } + } else if (s->GetType() == OggCodecState::TYPE_VORBIS && + ReadHeaders(TrackInfo::kAudioTrack, s)) { + if (!mVorbisState) { + SetupTarget(&mVorbisState, s); + } else { + s->Deactivate(); + } + } else if (s->GetType() == OggCodecState::TYPE_OPUS && + ReadHeaders(TrackInfo::kAudioTrack, s)) { + if (mOpusEnabled) { + if (!mOpusState) { + SetupTarget(&mOpusState, s); + } else { + s->Deactivate(); + } + } else { + NS_WARNING( + "Opus decoding disabled." + " See media.opus.enabled in about:config"); + } + } else if (s->GetType() == OggCodecState::TYPE_FLAC && + ReadHeaders(TrackInfo::kAudioTrack, s)) { + if (!mFlacState) { + SetupTarget(&mFlacState, s); + } else { + s->Deactivate(); + } + } else if (s->GetType() == OggCodecState::TYPE_SKELETON && + !mSkeletonState) { + mSkeletonState = static_cast<SkeletonState*>(s); + } else { + // Deactivate any non-primary bitstreams. + s->Deactivate(); + } + } + } + + SetupTargetSkeleton(); + SetupMediaTracksInfo(serials); + + if (HasAudio() || HasVideo()) { + int64_t startTime = -1; + FindStartTime(startTime); + if (startTime >= 0) { + OGG_DEBUG("Detected stream start time %" PRId64, startTime); + mStartTime.emplace(startTime); + } + + if (mInfo.mMetadataDuration.isNothing() && + Resource(TrackInfo::kAudioTrack)->GetLength() >= 0) { + // We didn't get a duration from the index or a Content-Duration header. + // Seek to the end of file to find the end time. + int64_t length = Resource(TrackInfo::kAudioTrack)->GetLength(); + + MOZ_ASSERT(length > 0, "Must have a content length to get end time"); + + int64_t endTime = RangeEndTime(TrackInfo::kAudioTrack, length); + + if (endTime != -1) { + mInfo.mUnadjustedMetadataEndTime.emplace( + TimeUnit::FromMicroseconds(endTime)); + mInfo.mMetadataDuration.emplace( + TimeUnit::FromMicroseconds(endTime - mStartTime.refOr(0))); + OGG_DEBUG("Got Ogg duration from seeking to end %" PRId64, endTime); + } + } + if (mInfo.mMetadataDuration.isNothing()) { + mInfo.mMetadataDuration.emplace(TimeUnit::FromInfinity()); + } + if (HasAudio()) { + mInfo.mAudio.mDuration = mInfo.mMetadataDuration.ref(); + } + if (HasVideo()) { + mInfo.mVideo.mDuration = mInfo.mMetadataDuration.ref(); + } + } else { + OGG_DEBUG("no audio or video tracks"); + return NS_ERROR_FAILURE; + } + + OGG_DEBUG("success?!"); + return NS_OK; +} + +void OggDemuxer::SetChained() { + { + if (mIsChained) { + return; + } + mIsChained = true; + } + if (mOnSeekableEvent) { + mOnSeekableEvent->Notify(); + } +} + +bool OggDemuxer::ReadOggChain(const media::TimeUnit& aLastEndTime) { + bool chained = false; + OpusState* newOpusState = nullptr; + VorbisState* newVorbisState = nullptr; + FlacState* newFlacState = nullptr; + UniquePtr<MetadataTags> tags; + + if (HasVideo() || HasSkeleton() || !HasAudio()) { + return false; + } + + tainted_ogg<ogg_page*> page = mSandbox->malloc_in_sandbox<ogg_page>(); + if (!page) { + return false; + } + auto clean_page = MakeScopeExit([&] { mSandbox->free_in_sandbox(page); }); + if (!ReadOggPage(TrackInfo::kAudioTrack, page.to_opaque()) || + !sandbox_invoke(*mSandbox, ogg_page_bos, page) + .unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON)) { + // Chaining is only supported for audio only ogg files. + return false; + } + + uint32_t serial = static_cast<uint32_t>( + sandbox_invoke(*mSandbox, ogg_page_serialno, page) + .unverified_safe_because( + "We are reading a new page with a serial number for the first " + "time and will check if we have seen it before prior to use.")); + if (mCodecStore.Contains(serial)) { + return false; + } + + UniquePtr<OggCodecState> codecState( + OggCodecState::Create(mSandbox.get(), page.to_opaque(), serial)); + if (!codecState) { + return false; + } + + if (mVorbisState && (codecState->GetType() == OggCodecState::TYPE_VORBIS)) { + newVorbisState = static_cast<VorbisState*>(codecState.get()); + } else if (mOpusState && + (codecState->GetType() == OggCodecState::TYPE_OPUS)) { + newOpusState = static_cast<OpusState*>(codecState.get()); + } else if (mFlacState && + (codecState->GetType() == OggCodecState::TYPE_FLAC)) { + newFlacState = static_cast<FlacState*>(codecState.get()); + } else { + return false; + } + + OggCodecState* state; + + mCodecStore.Add(serial, std::move(codecState)); + state = mCodecStore.Get(serial); + + NS_ENSURE_TRUE(state != nullptr, false); + + if (NS_FAILED(state->PageIn(page.to_opaque()))) { + return false; + } + + MessageField* msgInfo = nullptr; + if (mSkeletonState) { + mSkeletonState->mMsgFieldStore.Get(serial, &msgInfo); + } + + if ((newVorbisState && ReadHeaders(TrackInfo::kAudioTrack, newVorbisState)) && + (mVorbisState->GetInfo()->GetAsAudioInfo()->mRate == + newVorbisState->GetInfo()->GetAsAudioInfo()->mRate) && + (mVorbisState->GetInfo()->GetAsAudioInfo()->mChannels == + newVorbisState->GetInfo()->GetAsAudioInfo()->mChannels)) { + SetupTarget(&mVorbisState, newVorbisState); + OGG_DEBUG("New vorbis ogg link, serial=%d\n", mVorbisState->mSerial); + + if (msgInfo) { + InitTrack(msgInfo, &mInfo.mAudio, true); + } + + chained = true; + tags = newVorbisState->GetTags(); + } + + if ((newOpusState && ReadHeaders(TrackInfo::kAudioTrack, newOpusState)) && + (mOpusState->GetInfo()->GetAsAudioInfo()->mRate == + newOpusState->GetInfo()->GetAsAudioInfo()->mRate) && + (mOpusState->GetInfo()->GetAsAudioInfo()->mChannels == + newOpusState->GetInfo()->GetAsAudioInfo()->mChannels)) { + SetupTarget(&mOpusState, newOpusState); + + if (msgInfo) { + InitTrack(msgInfo, &mInfo.mAudio, true); + } + + chained = true; + tags = newOpusState->GetTags(); + } + + if ((newFlacState && ReadHeaders(TrackInfo::kAudioTrack, newFlacState)) && + (mFlacState->GetInfo()->GetAsAudioInfo()->mRate == + newFlacState->GetInfo()->GetAsAudioInfo()->mRate) && + (mFlacState->GetInfo()->GetAsAudioInfo()->mChannels == + newFlacState->GetInfo()->GetAsAudioInfo()->mChannels)) { + SetupTarget(&mFlacState, newFlacState); + OGG_DEBUG("New flac ogg link, serial=%d\n", mFlacState->mSerial); + + if (msgInfo) { + InitTrack(msgInfo, &mInfo.mAudio, true); + } + + chained = true; + tags = newFlacState->GetTags(); + } + + if (chained) { + SetChained(); + mInfo.mMediaSeekable = false; + mDecodedAudioDuration += aLastEndTime; + if (mTimedMetadataEvent) { + mTimedMetadataEvent->Notify( + TimedMetadata(mDecodedAudioDuration, std::move(tags), + UniquePtr<MediaInfo>(new MediaInfo(mInfo)))); + } + // Setup a new TrackInfo so that the MediaFormatReader will flush the + // current decoder. + mSharedAudioTrackInfo = + new TrackInfoSharedPtr(mInfo.mAudio, ++sStreamSourceID); + return true; + } + + return false; +} + +OggDemuxer::OggStateContext& OggDemuxer::OggState(TrackInfo::TrackType aType) { + if (aType == TrackInfo::kVideoTrack) { + return mVideoOggState; + } + return mAudioOggState; +} + +tainted_opaque_ogg<ogg_sync_state*> OggDemuxer::OggSyncState( + TrackInfo::TrackType aType) { + return OggState(aType).mOggState.mState; +} + +MediaResourceIndex* OggDemuxer::Resource(TrackInfo::TrackType aType) { + return &OggState(aType).mResource; +} + +MediaResourceIndex* OggDemuxer::CommonResource() { + return &mAudioOggState.mResource; +} + +bool OggDemuxer::ReadOggPage(TrackInfo::TrackType aType, + tainted_opaque_ogg<ogg_page*> aPage) { + int ret = 0; + while ((ret = sandbox_invoke(*mSandbox, ogg_sync_pageseek, + OggSyncState(aType), aPage) + .unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON)) <= + 0) { + if (ret < 0) { + // Lost page sync, have to skip up to next page. + continue; + } + // Returns a buffer that can be written too + // with the given size. This buffer is stored + // in the ogg synchronisation structure. + const uint32_t MIN_BUFFER_SIZE = 4096; + tainted_ogg<char*> buffer_tainted = sandbox_invoke( + *mSandbox, ogg_sync_buffer, OggSyncState(aType), MIN_BUFFER_SIZE); + MOZ_ASSERT(buffer_tainted != nullptr, "ogg_sync_buffer failed"); + + // Read from the resource into the buffer + uint32_t bytesRead = 0; + + char* buffer = buffer_tainted.copy_and_verify_buffer_address( + [](uintptr_t val) { return reinterpret_cast<char*>(val); }, + MIN_BUFFER_SIZE); + + nsresult rv = Resource(aType)->Read(buffer, MIN_BUFFER_SIZE, &bytesRead); + if (NS_FAILED(rv) || !bytesRead) { + // End of file or error. + return false; + } + + // Update the synchronisation layer with the number + // of bytes written to the buffer + ret = sandbox_invoke(*mSandbox, ogg_sync_wrote, OggSyncState(aType), + bytesRead) + .unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON); + NS_ENSURE_TRUE(ret == 0, false); + } + + return true; +} + +nsresult OggDemuxer::DemuxOggPage(TrackInfo::TrackType aType, + tainted_opaque_ogg<ogg_page*> aPage) { + tainted_ogg<int> serial = sandbox_invoke(*mSandbox, ogg_page_serialno, aPage); + OggCodecState* codecState = mCodecStore.Get(static_cast<uint32_t>( + serial.unverified_safe_because(RLBOX_OGG_PAGE_SERIAL_REASON))); + if (codecState == nullptr) { + OGG_DEBUG("encountered packet for unrecognized codecState"); + return NS_ERROR_FAILURE; + } + if (GetCodecStateType(codecState) != aType && + codecState->GetType() != OggCodecState::TYPE_SKELETON) { + // Not a page we're interested in. + return NS_OK; + } + if (NS_FAILED(codecState->PageIn(aPage))) { + OGG_DEBUG("codecState->PageIn failed"); + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +bool OggDemuxer::IsSeekable() const { + if (mIsChained) { + return false; + } + return true; +} + +UniquePtr<EncryptionInfo> OggDemuxer::GetCrypto() { return nullptr; } + +ogg_packet* OggDemuxer::GetNextPacket(TrackInfo::TrackType aType) { + OggCodecState* state = GetTrackCodecState(aType); + ogg_packet* packet = nullptr; + OggStateContext& context = OggState(aType); + + while (true) { + if (packet) { + Unused << state->PacketOut(); + } + DemuxUntilPacketAvailable(aType, state); + + packet = state->PacketPeek(); + if (!packet) { + break; + } + if (state->IsHeader(packet)) { + continue; + } + if (context.mNeedKeyframe && !state->IsKeyframe(packet)) { + continue; + } + context.mNeedKeyframe = false; + break; + } + + return packet; +} + +void OggDemuxer::DemuxUntilPacketAvailable(TrackInfo::TrackType aType, + OggCodecState* aState) { + while (!aState->IsPacketReady()) { + OGG_DEBUG("no packet yet, reading some more"); + tainted_ogg<ogg_page*> page = mSandbox->malloc_in_sandbox<ogg_page>(); + MOZ_RELEASE_ASSERT(page != nullptr); + auto clean_page = MakeScopeExit([&] { mSandbox->free_in_sandbox(page); }); + if (!ReadOggPage(aType, page.to_opaque())) { + OGG_DEBUG("no more pages to read in resource?"); + return; + } + DemuxOggPage(aType, page.to_opaque()); + } +} + +TimeIntervals OggDemuxer::GetBuffered(TrackInfo::TrackType aType) { + if (!HaveStartTime(aType)) { + return TimeIntervals(); + } + if (mIsChained) { + return TimeIntervals::Invalid(); + } + TimeIntervals buffered; + // HasAudio and HasVideo are not used here as they take a lock and cause + // a deadlock. Accessing mInfo doesn't require a lock - it doesn't change + // after metadata is read. + if (!mInfo.HasValidMedia()) { + // No need to search through the file if there are no audio or video tracks + return buffered; + } + + AutoPinned<MediaResource> resource(Resource(aType)->GetResource()); + MediaByteRangeSet ranges; + nsresult res = resource->GetCachedRanges(ranges); + NS_ENSURE_SUCCESS(res, TimeIntervals::Invalid()); + + const char time_interval_reason[] = + "Even if this computation is incorrect due to the reliance on tainted " + "values, only the search for the time interval or the time interval " + "returned will be affected. However this will not result in a memory " + "safety vulnerabilty in the Firefox renderer."; + + // Traverse across the buffered byte ranges, determining the time ranges + // they contain. MediaResource::GetNextCachedData(offset) returns -1 when + // offset is after the end of the media resource, or there's no more cached + // data after the offset. This loop will run until we've checked every + // buffered range in the media, in increasing order of offset. + nsAutoOggSyncState sync(mSandbox.get()); + for (uint32_t index = 0; index < ranges.Length(); index++) { + // Ensure the offsets are after the header pages. + int64_t startOffset = ranges[index].mStart; + int64_t endOffset = ranges[index].mEnd; + + // Because the granulepos time is actually the end time of the page, + // we special-case (startOffset == 0) so that the first + // buffered range always appears to be buffered from the media start + // time, rather than from the end-time of the first page. + int64_t startTime = (startOffset == 0) ? StartTime() : -1; + + // Find the start time of the range. Read pages until we find one with a + // granulepos which we can convert into a timestamp to use as the time of + // the start of the buffered range. + sandbox_invoke(*mSandbox, ogg_sync_reset, sync.mState); + tainted_ogg<ogg_page*> page = mSandbox->malloc_in_sandbox<ogg_page>(); + if (!page) { + return TimeIntervals::Invalid(); + } + auto clean_page = MakeScopeExit([&] { mSandbox->free_in_sandbox(page); }); + + while (startTime == -1) { + int32_t discard; + PageSyncResult pageSyncResult = + PageSync(mSandbox.get(), Resource(aType), sync.mState, true, + startOffset, endOffset, page, discard); + if (pageSyncResult == PAGE_SYNC_ERROR) { + return TimeIntervals::Invalid(); + } else if (pageSyncResult == PAGE_SYNC_END_OF_RANGE) { + // Hit the end of range without reading a page, give up trying to + // find a start time for this buffered range, skip onto the next one. + break; + } + + int64_t granulepos = sandbox_invoke(*mSandbox, ogg_page_granulepos, page) + .unverified_safe_because(time_interval_reason); + if (granulepos == -1) { + // Page doesn't have an end time, advance to the next page + // until we find one. + + bool failedPageLenVerify = false; + // Page length should be under 64Kb according to + // https://xiph.org/ogg/doc/libogg/ogg_page.html + long pageLength = + CopyAndVerifyOrFail(page->header_len + page->body_len, + val <= 64 * 1024, &failedPageLenVerify); + if (failedPageLenVerify) { + return TimeIntervals::Invalid(); + } + + startOffset += pageLength; + continue; + } + + tainted_ogg<uint32_t> serial = rlbox::sandbox_static_cast<uint32_t>( + sandbox_invoke(*mSandbox, ogg_page_serialno, page)); + if (aType == TrackInfo::kAudioTrack && mVorbisState && + (serial == mVorbisState->mSerial) + .unverified_safe_because(time_interval_reason)) { + startTime = mVorbisState->Time(granulepos); + MOZ_ASSERT(startTime > 0, "Must have positive start time"); + } else if (aType == TrackInfo::kAudioTrack && mOpusState && + (serial == mOpusState->mSerial) + .unverified_safe_because(time_interval_reason)) { + startTime = mOpusState->Time(granulepos); + MOZ_ASSERT(startTime > 0, "Must have positive start time"); + } else if (aType == TrackInfo::kAudioTrack && mFlacState && + (serial == mFlacState->mSerial) + .unverified_safe_because(time_interval_reason)) { + startTime = mFlacState->Time(granulepos); + MOZ_ASSERT(startTime > 0, "Must have positive start time"); + } else if (aType == TrackInfo::kVideoTrack && mTheoraState && + (serial == mTheoraState->mSerial) + .unverified_safe_because(time_interval_reason)) { + startTime = mTheoraState->Time(granulepos); + MOZ_ASSERT(startTime > 0, "Must have positive start time"); + } else if (mCodecStore.Contains( + serial.unverified_safe_because(time_interval_reason))) { + // Stream is not the theora or vorbis stream we're playing, + // but is one that we have header data for. + + bool failedPageLenVerify = false; + // Page length should be under 64Kb according to + // https://xiph.org/ogg/doc/libogg/ogg_page.html + long pageLength = + CopyAndVerifyOrFail(page->header_len + page->body_len, + val <= 64 * 1024, &failedPageLenVerify); + if (failedPageLenVerify) { + return TimeIntervals::Invalid(); + } + + startOffset += pageLength; + continue; + } else { + // Page is for a stream we don't know about (possibly a chained + // ogg), return OK to abort the finding any further ranges. This + // prevents us searching through the rest of the media when we + // may not be able to extract timestamps from it. + SetChained(); + return buffered; + } + } + + if (startTime != -1) { + // We were able to find a start time for that range, see if we can + // find an end time. + int64_t endTime = RangeEndTime(aType, startOffset, endOffset, true); + if (endTime > startTime) { + buffered += + TimeInterval(TimeUnit::FromMicroseconds(startTime - StartTime()), + TimeUnit::FromMicroseconds(endTime - StartTime())); + } + } + } + + return buffered; +} + +void OggDemuxer::FindStartTime(int64_t& aOutStartTime) { + // Extract the start times of the bitstreams in order to calculate + // the duration. + int64_t videoStartTime = INT64_MAX; + int64_t audioStartTime = INT64_MAX; + + if (HasVideo()) { + FindStartTime(TrackInfo::kVideoTrack, videoStartTime); + if (videoStartTime != INT64_MAX) { + OGG_DEBUG("OggDemuxer::FindStartTime() video=%" PRId64, videoStartTime); + mVideoOggState.mStartTime = + Some(TimeUnit::FromMicroseconds(videoStartTime)); + } + } + if (HasAudio()) { + FindStartTime(TrackInfo::kAudioTrack, audioStartTime); + if (audioStartTime != INT64_MAX) { + OGG_DEBUG("OggDemuxer::FindStartTime() audio=%" PRId64, audioStartTime); + mAudioOggState.mStartTime = + Some(TimeUnit::FromMicroseconds(audioStartTime)); + } + } + + int64_t startTime = std::min(videoStartTime, audioStartTime); + if (startTime != INT64_MAX) { + aOutStartTime = startTime; + } +} + +void OggDemuxer::FindStartTime(TrackInfo::TrackType aType, + int64_t& aOutStartTime) { + int64_t startTime = INT64_MAX; + + OggCodecState* state = GetTrackCodecState(aType); + ogg_packet* pkt = GetNextPacket(aType); + if (pkt) { + startTime = state->PacketStartTime(pkt); + } + + if (startTime != INT64_MAX) { + aOutStartTime = startTime; + } +} + +nsresult OggDemuxer::SeekInternal(TrackInfo::TrackType aType, + const TimeUnit& aTarget) { + int64_t target = aTarget.ToMicroseconds(); + OGG_DEBUG("About to seek to %" PRId64, target); + nsresult res; + int64_t adjustedTarget = target; + int64_t startTime = StartTime(aType); + int64_t endTime = mInfo.mMetadataDuration->ToMicroseconds() + startTime; + if (aType == TrackInfo::kAudioTrack && mOpusState) { + adjustedTarget = + std::max(startTime, target - OGG_SEEK_OPUS_PREROLL.ToMicroseconds()); + } + + if (!HaveStartTime(aType) || adjustedTarget == startTime) { + // We've seeked to the media start or we can't seek. + // Just seek to the offset of the first content page. + res = Resource(aType)->Seek(nsISeekableStream::NS_SEEK_SET, 0); + NS_ENSURE_SUCCESS(res, res); + + res = Reset(aType); + NS_ENSURE_SUCCESS(res, res); + } else { + // TODO: This may seek back unnecessarily far in the video, but we don't + // have a way of asking Skeleton to seek to a different target for each + // stream yet. Using adjustedTarget here is at least correct, if slow. + IndexedSeekResult sres = SeekToKeyframeUsingIndex(aType, adjustedTarget); + NS_ENSURE_TRUE(sres != SEEK_FATAL_ERROR, NS_ERROR_FAILURE); + if (sres == SEEK_INDEX_FAIL) { + // No index or other non-fatal index-related failure. Try to seek + // using a bisection search. Determine the already downloaded data + // in the media cache, so we can try to seek in the cached data first. + AutoTArray<SeekRange, 16> ranges; + res = GetSeekRanges(aType, ranges); + NS_ENSURE_SUCCESS(res, res); + + // Figure out if the seek target lies in a buffered range. + SeekRange r = + SelectSeekRange(aType, ranges, target, startTime, endTime, true); + + if (!r.IsNull()) { + // We know the buffered range in which the seek target lies, do a + // bisection search in that buffered range. + res = SeekInBufferedRange(aType, target, adjustedTarget, startTime, + endTime, ranges, r); + NS_ENSURE_SUCCESS(res, res); + } else { + // The target doesn't lie in a buffered range. Perform a bisection + // search over the whole media, using the known buffered ranges to + // reduce the search space. + res = SeekInUnbuffered(aType, target, startTime, endTime, ranges); + NS_ENSURE_SUCCESS(res, res); + } + } + } + + // Demux forwards until we find the first keyframe prior the target. + // there may be non-keyframes in the page before the keyframe. + // Additionally, we may have seeked to the first page referenced by the + // page index which may be quite far off the target. + // When doing fastSeek we display the first frame after the seek, so + // we need to advance the decode to the keyframe otherwise we'll get + // visual artifacts in the first frame output after the seek. + OggCodecState* state = GetTrackCodecState(aType); + OggPacketQueue tempPackets; + bool foundKeyframe = false; + while (true) { + DemuxUntilPacketAvailable(aType, state); + ogg_packet* packet = state->PacketPeek(); + if (packet == nullptr) { + OGG_DEBUG("End of stream reached before keyframe found in indexed seek"); + break; + } + int64_t startTstamp = state->PacketStartTime(packet); + if (foundKeyframe && startTstamp > adjustedTarget) { + break; + } + if (state->IsKeyframe(packet)) { + OGG_DEBUG("keyframe found after seeking at %" PRId64, startTstamp); + tempPackets.Erase(); + foundKeyframe = true; + } + if (foundKeyframe && startTstamp == adjustedTarget) { + break; + } + if (foundKeyframe) { + tempPackets.Append(state->PacketOut()); + } else { + // Discard video packets before the first keyframe. + Unused << state->PacketOut(); + } + } + // Re-add all packet into the codec state in order. + state->PushFront(std::move(tempPackets)); + + return NS_OK; +} + +OggDemuxer::IndexedSeekResult OggDemuxer::RollbackIndexedSeek( + TrackInfo::TrackType aType, int64_t aOffset) { + if (mSkeletonState) { + mSkeletonState->Deactivate(); + } + nsresult res = Resource(aType)->Seek(nsISeekableStream::NS_SEEK_SET, aOffset); + NS_ENSURE_SUCCESS(res, SEEK_FATAL_ERROR); + return SEEK_INDEX_FAIL; +} + +OggDemuxer::IndexedSeekResult OggDemuxer::SeekToKeyframeUsingIndex( + TrackInfo::TrackType aType, int64_t aTarget) { + if (!HasSkeleton() || !mSkeletonState->HasIndex()) { + return SEEK_INDEX_FAIL; + } + // We have an index from the Skeleton track, try to use it to seek. + AutoTArray<uint32_t, 2> tracks; + BuildSerialList(tracks); + SkeletonState::nsSeekTarget keyframe; + if (NS_FAILED(mSkeletonState->IndexedSeekTarget(aTarget, tracks, keyframe))) { + // Could not locate a keypoint for the target in the index. + return SEEK_INDEX_FAIL; + } + + // Remember original resource read cursor position so we can rollback on + // failure. + int64_t tell = Resource(aType)->Tell(); + + // Seek to the keypoint returned by the index. + if (keyframe.mKeyPoint.mOffset > Resource(aType)->GetLength() || + keyframe.mKeyPoint.mOffset < 0) { + // Index must be invalid. + return RollbackIndexedSeek(aType, tell); + } + OGG_DEBUG("Seeking using index to keyframe at offset %" PRId64 "\n", + keyframe.mKeyPoint.mOffset); + nsresult res = Resource(aType)->Seek(nsISeekableStream::NS_SEEK_SET, + keyframe.mKeyPoint.mOffset); + NS_ENSURE_SUCCESS(res, SEEK_FATAL_ERROR); + + // We've moved the read set, so reset decode. + res = Reset(aType); + NS_ENSURE_SUCCESS(res, SEEK_FATAL_ERROR); + + // Check that the page the index thinks is exactly here is actually exactly + // here. If not, the index is invalid. + tainted_ogg<ogg_page*> page = mSandbox->malloc_in_sandbox<ogg_page>(); + if (!page) { + return SEEK_INDEX_FAIL; + } + auto clean_page = MakeScopeExit([&] { mSandbox->free_in_sandbox(page); }); + int skippedBytes = 0; + PageSyncResult syncres = + PageSync(mSandbox.get(), Resource(aType), OggSyncState(aType), false, + keyframe.mKeyPoint.mOffset, Resource(aType)->GetLength(), page, + skippedBytes); + NS_ENSURE_TRUE(syncres != PAGE_SYNC_ERROR, SEEK_FATAL_ERROR); + if (syncres != PAGE_SYNC_OK || skippedBytes != 0) { + OGG_DEBUG( + "Indexed-seek failure: Ogg Skeleton Index is invalid " + "or sync error after seek"); + return RollbackIndexedSeek(aType, tell); + } + uint32_t serial = static_cast<uint32_t>( + sandbox_invoke(*mSandbox, ogg_page_serialno, page) + .unverified_safe_because( + "Serial is only used to locate the correct page. If the serial " + "is incorrect the the renderer would just fail to seek with an " + "error code. This would not lead to any memory safety bugs.")); + if (serial != keyframe.mSerial) { + // Serialno of page at offset isn't what the index told us to expect. + // Assume the index is invalid. + return RollbackIndexedSeek(aType, tell); + } + OggCodecState* codecState = mCodecStore.Get(serial); + if (codecState && codecState->mActive && + sandbox_invoke(*mSandbox, ogg_stream_pagein, codecState->mState, page) + .unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON) != 0) { + // Couldn't insert page into the ogg resource, or somehow the resource + // is no longer active. + return RollbackIndexedSeek(aType, tell); + } + return SEEK_OK; +} + +// Reads a page from the media resource. +OggDemuxer::PageSyncResult OggDemuxer::PageSync( + rlbox_sandbox_ogg* aSandbox, MediaResourceIndex* aResource, + tainted_opaque_ogg<ogg_sync_state*> aState, bool aCachedDataOnly, + int64_t aOffset, int64_t aEndOffset, tainted_ogg<ogg_page*> aPage, + int& aSkippedBytes) { + aSkippedBytes = 0; + // Sync to the next page. + tainted_ogg<int> ret = 0; + uint32_t bytesRead = 0; + int64_t readHead = aOffset; + while (ret.unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON) <= 0) { + tainted_ogg<long> seek_ret = + sandbox_invoke(*aSandbox, ogg_sync_pageseek, aState, aPage); + + // We aren't really verifying the value of seek_ret below. + // We are merely ensuring that it won't overflow an integer. + // However we are assigning the value to ret which is marked tainted, so + // this is fine. + bool failedVerify = false; + CheckedInt<int> checker; + ret = CopyAndVerifyOrFail( + seek_ret, (static_cast<void>(checker = val), checker.isValid()), + &failedVerify); + if (failedVerify) { + return PAGE_SYNC_ERROR; + } + + if (ret.unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON) == 0) { + const int page_step_val = PAGE_STEP; + tainted_ogg<char*> buffer_tainted = + sandbox_invoke(*aSandbox, ogg_sync_buffer, aState, page_step_val); + MOZ_ASSERT(buffer_tainted != nullptr, "Must have a buffer"); + + // Read from the file into the buffer + int64_t bytesToRead = + std::min(static_cast<int64_t>(PAGE_STEP), aEndOffset - readHead); + MOZ_ASSERT(bytesToRead <= UINT32_MAX, "bytesToRead range check"); + if (bytesToRead <= 0) { + return PAGE_SYNC_END_OF_RANGE; + } + char* buffer = buffer_tainted.copy_and_verify_buffer_address( + [](uintptr_t val) { return reinterpret_cast<char*>(val); }, + static_cast<size_t>(bytesToRead)); + + nsresult rv = NS_OK; + if (aCachedDataOnly) { + rv = aResource->GetResource()->ReadFromCache( + buffer, readHead, static_cast<uint32_t>(bytesToRead)); + NS_ENSURE_SUCCESS(rv, PAGE_SYNC_ERROR); + bytesRead = static_cast<uint32_t>(bytesToRead); + } else { + rv = aResource->Seek(nsISeekableStream::NS_SEEK_SET, readHead); + NS_ENSURE_SUCCESS(rv, PAGE_SYNC_ERROR); + rv = aResource->Read(buffer, static_cast<uint32_t>(bytesToRead), + &bytesRead); + NS_ENSURE_SUCCESS(rv, PAGE_SYNC_ERROR); + } + if (bytesRead == 0 && NS_SUCCEEDED(rv)) { + // End of file. + return PAGE_SYNC_END_OF_RANGE; + } + readHead += bytesRead; + + // Update the synchronisation layer with the number + // of bytes written to the buffer + ret = sandbox_invoke(*aSandbox, ogg_sync_wrote, aState, bytesRead); + NS_ENSURE_TRUE( + ret.unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON) == 0, + PAGE_SYNC_ERROR); + continue; + } + + if (ret.unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON) < 0) { + MOZ_ASSERT(aSkippedBytes >= 0, "Offset >= 0"); + bool failedSkippedBytesVerify = false; + ret.copy_and_verify([&](int val) { + int64_t result = static_cast<int64_t>(aSkippedBytes) - val; + if (result > std::numeric_limits<int>::max() || + result > (aEndOffset - aOffset) || result < 0) { + failedSkippedBytesVerify = true; + } else { + aSkippedBytes = result; + } + }); + if (failedSkippedBytesVerify) { + return PAGE_SYNC_ERROR; + } + continue; + } + } + + return PAGE_SYNC_OK; +} + +// OggTrackDemuxer +OggTrackDemuxer::OggTrackDemuxer(OggDemuxer* aParent, + TrackInfo::TrackType aType, + uint32_t aTrackNumber) + : mParent(aParent), mType(aType) { + mInfo = mParent->GetTrackInfo(aType, aTrackNumber); + MOZ_ASSERT(mInfo); +} + +OggTrackDemuxer::~OggTrackDemuxer() = default; + +UniquePtr<TrackInfo> OggTrackDemuxer::GetInfo() const { return mInfo->Clone(); } + +RefPtr<OggTrackDemuxer::SeekPromise> OggTrackDemuxer::Seek( + const TimeUnit& aTime) { + // Seeks to aTime. Upon success, SeekPromise will be resolved with the + // actual time seeked to. Typically the random access point time + mQueuedSample = nullptr; + TimeUnit seekTime = aTime; + if (mParent->SeekInternal(mType, aTime) == NS_OK) { + RefPtr<MediaRawData> sample(NextSample()); + + // Check what time we actually seeked to. + if (sample != nullptr) { + seekTime = sample->mTime; + OGG_DEBUG("%p seeked to time %" PRId64, this, seekTime.ToMicroseconds()); + } + mQueuedSample = sample; + + return SeekPromise::CreateAndResolve(seekTime, __func__); + } else { + return SeekPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_DEMUXER_ERR, + __func__); + } +} + +RefPtr<MediaRawData> OggTrackDemuxer::NextSample() { + if (mQueuedSample) { + RefPtr<MediaRawData> nextSample = mQueuedSample; + mQueuedSample = nullptr; + if (mType == TrackInfo::kAudioTrack) { + nextSample->mTrackInfo = mParent->mSharedAudioTrackInfo; + } + return nextSample; + } + ogg_packet* packet = mParent->GetNextPacket(mType); + if (!packet) { + return nullptr; + } + // Check the eos state in case we need to look for chained streams. + bool eos = packet->e_o_s; + OggCodecState* state = mParent->GetTrackCodecState(mType); + RefPtr<MediaRawData> data = state->PacketOutAsMediaRawData(); + // ogg allows 'nil' packets, that are EOS and of size 0. + if (!data || (data->mEOS && data->Size() == 0)) { + return nullptr; + } + if (mType == TrackInfo::kAudioTrack) { + data->mTrackInfo = mParent->mSharedAudioTrackInfo; + } + // mDecodedAudioDuration gets adjusted during ReadOggChain(). + TimeUnit totalDuration = mParent->mDecodedAudioDuration; + if (eos) { + // We've encountered an end of bitstream packet; check for a chained + // bitstream following this one. + // This will also update mSharedAudioTrackInfo. + mParent->ReadOggChain(data->GetEndTime()); + } + data->mOffset = mParent->Resource(mType)->Tell(); + // We adjust the start time of the sample to account for the potential ogg + // chaining. + data->mTime += totalDuration; + if (!data->mTime.IsValid()) { + return nullptr; + } + + return data; +} + +RefPtr<OggTrackDemuxer::SamplesPromise> OggTrackDemuxer::GetSamples( + int32_t aNumSamples) { + RefPtr<SamplesHolder> samples = new SamplesHolder; + if (!aNumSamples) { + return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_DEMUXER_ERR, + __func__); + } + + while (aNumSamples) { + RefPtr<MediaRawData> sample(NextSample()); + if (!sample) { + break; + } + if (!sample->HasValidTime()) { + return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_DEMUXER_ERR, + __func__); + } + samples->AppendSample(sample); + aNumSamples--; + } + + if (samples->GetSamples().IsEmpty()) { + return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_END_OF_STREAM, + __func__); + } else { + return SamplesPromise::CreateAndResolve(samples, __func__); + } +} + +void OggTrackDemuxer::Reset() { + mParent->Reset(mType); + mQueuedSample = nullptr; +} + +RefPtr<OggTrackDemuxer::SkipAccessPointPromise> +OggTrackDemuxer::SkipToNextRandomAccessPoint(const TimeUnit& aTimeThreshold) { + uint32_t parsed = 0; + bool found = false; + RefPtr<MediaRawData> sample; + + OGG_DEBUG("TimeThreshold: %f", aTimeThreshold.ToSeconds()); + while (!found && (sample = NextSample())) { + parsed++; + if (sample->mKeyframe && sample->mTime >= aTimeThreshold) { + found = true; + mQueuedSample = sample; + } + } + if (found) { + OGG_DEBUG("next sample: %f (parsed: %d)", sample->mTime.ToSeconds(), + parsed); + return SkipAccessPointPromise::CreateAndResolve(parsed, __func__); + } else { + SkipFailureHolder failure(NS_ERROR_DOM_MEDIA_END_OF_STREAM, parsed); + return SkipAccessPointPromise::CreateAndReject(std::move(failure), + __func__); + } +} + +TimeIntervals OggTrackDemuxer::GetBuffered() { + return mParent->GetBuffered(mType); +} + +void OggTrackDemuxer::BreakCycles() { mParent = nullptr; } + +// Returns an ogg page's checksum. +tainted_opaque_ogg<ogg_uint32_t> OggDemuxer::GetPageChecksum( + tainted_opaque_ogg<ogg_page*> aPage) { + tainted_ogg<ogg_page*> page = rlbox::from_opaque(aPage); + + const char hint_reason[] = + "Early bail out of checksum. Even if this is wrong, the renderer's " + "security is not compromised."; + if (page == nullptr || + (page->header == nullptr).unverified_safe_because(hint_reason) || + (page->header_len < 25).unverified_safe_because(hint_reason)) { + tainted_ogg<ogg_uint32_t> ret = 0; + return ret.to_opaque(); + } + + const int CHECKSUM_BYTES_LENGTH = 4; + const unsigned char* p = + (page->header + 22u) + .copy_and_verify_buffer_address( + [](uintptr_t val) { + return reinterpret_cast<const unsigned char*>(val); + }, + CHECKSUM_BYTES_LENGTH); + uint32_t c = + static_cast<uint32_t>(p[0] + (p[1] << 8) + (p[2] << 16) + (p[3] << 24)); + tainted_ogg<uint32_t> ret = c; + return ret.to_opaque(); +} + +int64_t OggDemuxer::RangeStartTime(TrackInfo::TrackType aType, + int64_t aOffset) { + int64_t position = Resource(aType)->Tell(); + nsresult res = Resource(aType)->Seek(nsISeekableStream::NS_SEEK_SET, aOffset); + NS_ENSURE_SUCCESS(res, 0); + int64_t startTime = 0; + FindStartTime(aType, startTime); + res = Resource(aType)->Seek(nsISeekableStream::NS_SEEK_SET, position); + NS_ENSURE_SUCCESS(res, -1); + return startTime; +} + +struct nsDemuxerAutoOggSyncState { + explicit nsDemuxerAutoOggSyncState(rlbox_sandbox_ogg& aSandbox) + : mSandbox(aSandbox) { + mState = mSandbox.malloc_in_sandbox<ogg_sync_state>(); + MOZ_RELEASE_ASSERT(mState != nullptr); + sandbox_invoke(mSandbox, ogg_sync_init, mState); + } + ~nsDemuxerAutoOggSyncState() { + sandbox_invoke(mSandbox, ogg_sync_clear, mState); + mSandbox.free_in_sandbox(mState); + } + rlbox_sandbox_ogg& mSandbox; + tainted_ogg<ogg_sync_state*> mState; +}; + +int64_t OggDemuxer::RangeEndTime(TrackInfo::TrackType aType, + int64_t aEndOffset) { + int64_t position = Resource(aType)->Tell(); + int64_t endTime = RangeEndTime(aType, 0, aEndOffset, false); + nsresult res = + Resource(aType)->Seek(nsISeekableStream::NS_SEEK_SET, position); + NS_ENSURE_SUCCESS(res, -1); + return endTime; +} + +int64_t OggDemuxer::RangeEndTime(TrackInfo::TrackType aType, + int64_t aStartOffset, int64_t aEndOffset, + bool aCachedDataOnly) { + nsDemuxerAutoOggSyncState sync(*mSandbox); + + // We need to find the last page which ends before aEndOffset that + // has a granulepos that we can convert to a timestamp. We do this by + // backing off from aEndOffset until we encounter a page on which we can + // interpret the granulepos. If while backing off we encounter a page which + // we've previously encountered before, we'll either backoff again if we + // haven't found an end time yet, or return the last end time found. + const int step = 5000; + const int maxOggPageSize = 65306; + int64_t readStartOffset = aEndOffset; + int64_t readLimitOffset = aEndOffset; + int64_t readHead = aEndOffset; + int64_t endTime = -1; + uint32_t checksumAfterSeek = 0; + uint32_t prevChecksumAfterSeek = 0; + bool mustBackOff = false; + tainted_ogg<ogg_page*> page = mSandbox->malloc_in_sandbox<ogg_page>(); + if (!page) { + return -1; + } + auto clean_page = MakeScopeExit([&] { mSandbox->free_in_sandbox(page); }); + while (true) { + tainted_ogg<long> seek_ret = + sandbox_invoke(*mSandbox, ogg_sync_pageseek, sync.mState, page); + + // We aren't really verifying the value of seek_ret below. + // We are merely ensuring that it won't overflow an integer. + // However we are assigning the value to ret which is marked tainted, so + // this is fine. + bool failedVerify = false; + CheckedInt<int> checker; + tainted_ogg<int> ret = CopyAndVerifyOrFail( + seek_ret, (static_cast<void>(checker = val), checker.isValid()), + &failedVerify); + if (failedVerify) { + return -1; + } + + if (ret.unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON) == 0) { + // We need more data if we've not encountered a page we've seen before, + // or we've read to the end of file. + if (mustBackOff || readHead == aEndOffset || readHead == aStartOffset) { + if (endTime != -1 || readStartOffset == 0) { + // We have encountered a page before, or we're at the end of file. + break; + } + mustBackOff = false; + prevChecksumAfterSeek = checksumAfterSeek; + checksumAfterSeek = 0; + sandbox_invoke(*mSandbox, ogg_sync_reset, sync.mState); + readStartOffset = + std::max(static_cast<int64_t>(0), readStartOffset - step); + // There's no point reading more than the maximum size of + // an Ogg page into data we've previously scanned. Any data + // between readLimitOffset and aEndOffset must be garbage + // and we can ignore it thereafter. + readLimitOffset = + std::min(readLimitOffset, readStartOffset + maxOggPageSize); + readHead = std::max(aStartOffset, readStartOffset); + } + + int64_t limit = + std::min(static_cast<int64_t>(UINT32_MAX), aEndOffset - readHead); + limit = std::max(static_cast<int64_t>(0), limit); + limit = std::min(limit, static_cast<int64_t>(step)); + uint32_t bytesToRead = static_cast<uint32_t>(limit); + uint32_t bytesRead = 0; + tainted_ogg<char*> buffer_tainted = + sandbox_invoke(*mSandbox, ogg_sync_buffer, sync.mState, bytesToRead); + char* buffer = buffer_tainted.copy_and_verify_buffer_address( + [](uintptr_t val) { return reinterpret_cast<char*>(val); }, + bytesToRead); + MOZ_ASSERT(buffer, "Must have buffer"); + nsresult res; + if (aCachedDataOnly) { + res = Resource(aType)->GetResource()->ReadFromCache(buffer, readHead, + bytesToRead); + NS_ENSURE_SUCCESS(res, -1); + bytesRead = bytesToRead; + } else { + MOZ_ASSERT(readHead < aEndOffset, + "resource pos must be before range end"); + res = Resource(aType)->Seek(nsISeekableStream::NS_SEEK_SET, readHead); + NS_ENSURE_SUCCESS(res, -1); + res = Resource(aType)->Read(buffer, bytesToRead, &bytesRead); + NS_ENSURE_SUCCESS(res, -1); + } + readHead += bytesRead; + if (readHead > readLimitOffset) { + mustBackOff = true; + } + + // Update the synchronisation layer with the number + // of bytes written to the buffer + ret = sandbox_invoke(*mSandbox, ogg_sync_wrote, sync.mState, bytesRead); + bool failedWroteVerify = false; + int wrote_success = + CopyAndVerifyOrFail(ret, val == 0 || val == -1, &failedWroteVerify); + if (failedWroteVerify) { + return -1; + } + + if (wrote_success != 0) { + endTime = -1; + break; + } + continue; + } + + if (ret.unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON) < 0 || + sandbox_invoke(*mSandbox, ogg_page_granulepos, page) + .unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON) < 0) { + continue; + } + + tainted_ogg<uint32_t> checksum_tainted = + rlbox::from_opaque(GetPageChecksum(page.to_opaque())); + uint32_t checksum = checksum_tainted.unverified_safe_because( + "checksum is only being used as a hint as part of search for end time. " + "Incorrect values will not affect the memory safety of the renderer."); + if (checksumAfterSeek == 0) { + // This is the first page we've decoded after a backoff/seek. Remember + // the page checksum. If we backoff further and encounter this page + // again, we'll know that we won't find a page with an end time after + // this one, so we'll know to back off again. + checksumAfterSeek = checksum; + } + if (checksum == prevChecksumAfterSeek) { + // This page has the same checksum as the first page we encountered + // after the last backoff/seek. Since we've already scanned after this + // page and failed to find an end time, we may as well backoff again and + // try to find an end time from an earlier page. + mustBackOff = true; + continue; + } + + int64_t granulepos = + sandbox_invoke(*mSandbox, ogg_page_granulepos, page) + .unverified_safe_because( + "If this is incorrect it may lead to incorrect seeking " + "behavior in the stream, however will not affect the memory " + "safety of the Firefox renderer."); + uint32_t serial = static_cast<uint32_t>( + sandbox_invoke(*mSandbox, ogg_page_serialno, page) + .unverified_safe_because(RLBOX_OGG_PAGE_SERIAL_REASON)); + + OggCodecState* codecState = nullptr; + codecState = mCodecStore.Get(serial); + if (!codecState) { + // This page is from a bitstream which we haven't encountered yet. + // It's probably from a new "link" in a "chained" ogg. Don't + // bother even trying to find a duration... + SetChained(); + endTime = -1; + break; + } + + int64_t t = codecState->Time(granulepos); + if (t != -1) { + endTime = t; + } + } + + return endTime; +} + +nsresult OggDemuxer::GetSeekRanges(TrackInfo::TrackType aType, + nsTArray<SeekRange>& aRanges) { + AutoPinned<MediaResource> resource(Resource(aType)->GetResource()); + MediaByteRangeSet cached; + nsresult res = resource->GetCachedRanges(cached); + NS_ENSURE_SUCCESS(res, res); + + for (uint32_t index = 0; index < cached.Length(); index++) { + auto& range = cached[index]; + int64_t startTime = -1; + int64_t endTime = -1; + if (NS_FAILED(Reset(aType))) { + return NS_ERROR_FAILURE; + } + int64_t startOffset = range.mStart; + int64_t endOffset = range.mEnd; + startTime = RangeStartTime(aType, startOffset); + if (startTime != -1 && ((endTime = RangeEndTime(aType, endOffset)) != -1)) { + NS_WARNING_ASSERTION(startTime < endTime, + "Start time must be before end time"); + aRanges.AppendElement( + SeekRange(startOffset, endOffset, startTime, endTime)); + } + } + if (NS_FAILED(Reset(aType))) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +OggDemuxer::SeekRange OggDemuxer::SelectSeekRange( + TrackInfo::TrackType aType, const nsTArray<SeekRange>& ranges, + int64_t aTarget, int64_t aStartTime, int64_t aEndTime, bool aExact) { + int64_t so = 0; + int64_t eo = Resource(aType)->GetLength(); + int64_t st = aStartTime; + int64_t et = aEndTime; + for (uint32_t i = 0; i < ranges.Length(); i++) { + const SeekRange& r = ranges[i]; + if (r.mTimeStart < aTarget) { + so = r.mOffsetStart; + st = r.mTimeStart; + } + if (r.mTimeEnd >= aTarget && r.mTimeEnd < et) { + eo = r.mOffsetEnd; + et = r.mTimeEnd; + } + + if (r.mTimeStart < aTarget && aTarget <= r.mTimeEnd) { + // Target lies exactly in this range. + return ranges[i]; + } + } + if (aExact || eo == -1) { + return SeekRange(); + } + return SeekRange(so, eo, st, et); +} + +nsresult OggDemuxer::SeekInBufferedRange(TrackInfo::TrackType aType, + int64_t aTarget, + int64_t aAdjustedTarget, + int64_t aStartTime, int64_t aEndTime, + const nsTArray<SeekRange>& aRanges, + const SeekRange& aRange) { + OGG_DEBUG("Seeking in buffered data to %" PRId64 " using bisection search", + aTarget); + if (aType == TrackInfo::kVideoTrack || aAdjustedTarget >= aTarget) { + // We know the exact byte range in which the target must lie. It must + // be buffered in the media cache. Seek there. + nsresult res = SeekBisection(aType, aTarget, aRange, 0); + if (NS_FAILED(res) || aType != TrackInfo::kVideoTrack) { + return res; + } + + // We have an active Theora bitstream. Peek the next Theora frame, and + // extract its keyframe's time. + DemuxUntilPacketAvailable(aType, mTheoraState); + ogg_packet* packet = mTheoraState->PacketPeek(); + if (packet && !mTheoraState->IsKeyframe(packet)) { + // First post-seek frame isn't a keyframe, seek back to previous keyframe, + // otherwise we'll get visual artifacts. + MOZ_ASSERT(packet->granulepos != -1, "Must have a granulepos"); + int shift = mTheoraState->KeyFrameGranuleJobs(); + int64_t keyframeGranulepos = (packet->granulepos >> shift) << shift; + int64_t keyframeTime = mTheoraState->StartTime(keyframeGranulepos); + SEEK_LOG(LogLevel::Debug, + ("Keyframe for %lld is at %lld, seeking back to it", frameTime, + keyframeTime)); + aAdjustedTarget = std::min(aAdjustedTarget, keyframeTime); + } + } + + nsresult res = NS_OK; + if (aAdjustedTarget < aTarget) { + SeekRange k = SelectSeekRange(aType, aRanges, aAdjustedTarget, aStartTime, + aEndTime, false); + res = SeekBisection(aType, aAdjustedTarget, k, OGG_SEEK_FUZZ_USECS); + } + return res; +} + +nsresult OggDemuxer::SeekInUnbuffered(TrackInfo::TrackType aType, + int64_t aTarget, int64_t aStartTime, + int64_t aEndTime, + const nsTArray<SeekRange>& aRanges) { + OGG_DEBUG("Seeking in unbuffered data to %" PRId64 " using bisection search", + aTarget); + + // If we've got an active Theora bitstream, determine the maximum possible + // time in usecs which a keyframe could be before a given interframe. We + // subtract this from our seek target, seek to the new target, and then + // will decode forward to the original seek target. We should encounter a + // keyframe in that interval. This prevents us from needing to run two + // bisections; one for the seek target frame, and another to find its + // keyframe. It's usually faster to just download this extra data, rather + // tham perform two bisections to find the seek target's keyframe. We + // don't do this offsetting when seeking in a buffered range, + // as the extra decoding causes a noticeable speed hit when all the data + // is buffered (compared to just doing a bisection to exactly find the + // keyframe). + int64_t keyframeOffsetMs = 0; + if (aType == TrackInfo::kVideoTrack && mTheoraState) { + keyframeOffsetMs = mTheoraState->MaxKeyframeOffset(); + } + // Add in the Opus pre-roll if necessary, as well. + if (aType == TrackInfo::kAudioTrack && mOpusState) { + keyframeOffsetMs = + std::max(keyframeOffsetMs, OGG_SEEK_OPUS_PREROLL.ToMilliseconds()); + } + int64_t seekTarget = std::max(aStartTime, aTarget - keyframeOffsetMs); + // Minimize the bisection search space using the known timestamps from the + // buffered ranges. + SeekRange k = + SelectSeekRange(aType, aRanges, seekTarget, aStartTime, aEndTime, false); + return SeekBisection(aType, seekTarget, k, OGG_SEEK_FUZZ_USECS); +} + +nsresult OggDemuxer::SeekBisection(TrackInfo::TrackType aType, int64_t aTarget, + const SeekRange& aRange, uint32_t aFuzz) { + nsresult res; + + if (aTarget <= aRange.mTimeStart) { + if (NS_FAILED(Reset(aType))) { + return NS_ERROR_FAILURE; + } + res = Resource(aType)->Seek(nsISeekableStream::NS_SEEK_SET, 0); + NS_ENSURE_SUCCESS(res, res); + return NS_OK; + } + + // Bisection search, find start offset of last page with end time less than + // the seek target. + ogg_int64_t startOffset = aRange.mOffsetStart; + ogg_int64_t startTime = aRange.mTimeStart; + ogg_int64_t startLength = 0; // Length of the page at startOffset. + ogg_int64_t endOffset = aRange.mOffsetEnd; + ogg_int64_t endTime = aRange.mTimeEnd; + + ogg_int64_t seekTarget = aTarget; + int64_t seekLowerBound = std::max(static_cast<int64_t>(0), aTarget - aFuzz); + int hops = 0; + DebugOnly<ogg_int64_t> previousGuess = -1; + int backsteps = 0; + const int maxBackStep = 10; + MOZ_ASSERT( + static_cast<uint64_t>(PAGE_STEP) * pow(2.0, maxBackStep) < INT32_MAX, + "Backstep calculation must not overflow"); + + // Seek via bisection search. Loop until we find the offset where the page + // before the offset is before the seek target, and the page after the offset + // is after the seek target. + tainted_ogg<ogg_page*> page = mSandbox->malloc_in_sandbox<ogg_page>(); + if (!page) { + return NS_ERROR_OUT_OF_MEMORY; + } + auto clean_page = MakeScopeExit([&] { mSandbox->free_in_sandbox(page); }); + while (true) { + ogg_int64_t duration = 0; + double target = 0; + ogg_int64_t interval = 0; + ogg_int64_t guess = 0; + int skippedBytes = 0; + ogg_int64_t pageOffset = 0; + ogg_int64_t pageLength = 0; + ogg_int64_t granuleTime = -1; + bool mustBackoff = false; + + // Guess where we should bisect to, based on the bit rate and the time + // remaining in the interval. Loop until we can determine the time at + // the guess offset. + while (true) { + // Discard any previously buffered packets/pages. + if (NS_FAILED(Reset(aType))) { + return NS_ERROR_FAILURE; + } + + interval = endOffset - startOffset - startLength; + if (interval == 0) { + // Our interval is empty, we've found the optimal seek point, as the + // page at the start offset is before the seek target, and the page + // at the end offset is after the seek target. + SEEK_LOG(LogLevel::Debug, + ("Interval narrowed, terminating bisection.")); + break; + } + + // Guess bisection point. + duration = endTime - startTime; + target = (double)(seekTarget - startTime) / (double)duration; + guess = startOffset + startLength + + static_cast<ogg_int64_t>((double)interval * target); + guess = std::min(guess, endOffset - PAGE_STEP); + if (mustBackoff) { + // We previously failed to determine the time at the guess offset, + // probably because we ran out of data to decode. This usually happens + // when we guess very close to the end offset. So reduce the guess + // offset using an exponential backoff until we determine the time. + SEEK_LOG( + LogLevel::Debug, + ("Backing off %d bytes, backsteps=%d", + static_cast<int32_t>(PAGE_STEP * pow(2.0, backsteps)), backsteps)); + guess -= PAGE_STEP * static_cast<ogg_int64_t>(pow(2.0, backsteps)); + + if (guess <= startOffset) { + // We've tried to backoff to before the start offset of our seek + // range. This means we couldn't find a seek termination position + // near the end of the seek range, so just set the seek termination + // condition, and break out of the bisection loop. We'll begin + // decoding from the start of the seek range. + interval = 0; + break; + } + + backsteps = std::min(backsteps + 1, maxBackStep); + // We reset mustBackoff. If we still need to backoff further, it will + // be set to true again. + mustBackoff = false; + } else { + backsteps = 0; + } + guess = std::max(guess, startOffset + startLength); + + SEEK_LOG(LogLevel::Debug, + ("Seek loop start[o=%lld..%lld t=%lld] " + "end[o=%lld t=%lld] " + "interval=%lld target=%lf guess=%lld", + startOffset, (startOffset + startLength), startTime, endOffset, + endTime, interval, target, guess)); + + MOZ_ASSERT(guess >= startOffset + startLength, + "Guess must be after range start"); + MOZ_ASSERT(guess < endOffset, "Guess must be before range end"); + MOZ_ASSERT(guess != previousGuess, + "Guess should be different to previous"); + previousGuess = guess; + + hops++; + + // Locate the next page after our seek guess, and then figure out the + // granule time of the audio and video bitstreams there. We can then + // make a bisection decision based on our location in the media. + PageSyncResult pageSyncResult = + PageSync(mSandbox.get(), Resource(aType), OggSyncState(aType), false, + guess, endOffset, page, skippedBytes); + NS_ENSURE_TRUE(pageSyncResult != PAGE_SYNC_ERROR, NS_ERROR_FAILURE); + + if (pageSyncResult == PAGE_SYNC_END_OF_RANGE) { + // Our guess was too close to the end, we've ended up reading the end + // page. Backoff exponentially from the end point, in case the last + // page/frame/sample is huge. + mustBackoff = true; + SEEK_LOG(LogLevel::Debug, ("Hit the end of range, backing off")); + continue; + } + + // We've located a page of length |ret| at |guess + skippedBytes|. + // Remember where the page is located. + pageOffset = guess + skippedBytes; + + bool failedPageLenVerify = false; + // Page length should be under 64Kb according to + // https://xiph.org/ogg/doc/libogg/ogg_page.html + pageLength = CopyAndVerifyOrFail(page->header_len + page->body_len, + val <= 64 * 1024, &failedPageLenVerify); + if (failedPageLenVerify) { + return NS_ERROR_FAILURE; + } + + // Read pages until we can determine the granule time of the audio and + // video bitstream. + ogg_int64_t audioTime = -1; + ogg_int64_t videoTime = -1; + do { + // Add the page to its codec state, determine its granule time. + uint32_t serial = static_cast<uint32_t>( + sandbox_invoke(*mSandbox, ogg_page_serialno, page) + .unverified_safe_because(RLBOX_OGG_PAGE_SERIAL_REASON)); + OggCodecState* codecState = mCodecStore.Get(serial); + if (codecState && GetCodecStateType(codecState) == aType) { + if (codecState->mActive) { + int ret = + sandbox_invoke(*mSandbox, ogg_stream_pagein, codecState->mState, + page) + .unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON); + NS_ENSURE_TRUE(ret == 0, NS_ERROR_FAILURE); + } + + ogg_int64_t granulepos = + sandbox_invoke(*mSandbox, ogg_page_granulepos, page) + .unverified_safe_because( + "If this is incorrect it may lead to incorrect seeking " + "behavior in the stream, however will not affect the " + "memory safety of the Firefox renderer."); + + if (aType == TrackInfo::kAudioTrack && granulepos > 0 && + audioTime == -1) { + if (mVorbisState && serial == mVorbisState->mSerial) { + audioTime = mVorbisState->Time(granulepos); + } else if (mOpusState && serial == mOpusState->mSerial) { + audioTime = mOpusState->Time(granulepos); + } else if (mFlacState && serial == mFlacState->mSerial) { + audioTime = mFlacState->Time(granulepos); + } + } + + if (aType == TrackInfo::kVideoTrack && granulepos > 0 && + serial == mTheoraState->mSerial && videoTime == -1) { + videoTime = mTheoraState->Time(granulepos); + } + + if (pageOffset + pageLength >= endOffset) { + // Hit end of readable data. + break; + } + } + if (!ReadOggPage(aType, page.to_opaque())) { + break; + } + + } while ((aType == TrackInfo::kAudioTrack && audioTime == -1) || + (aType == TrackInfo::kVideoTrack && videoTime == -1)); + + if ((aType == TrackInfo::kAudioTrack && audioTime == -1) || + (aType == TrackInfo::kVideoTrack && videoTime == -1)) { + // We don't have timestamps for all active tracks... + if (pageOffset == startOffset + startLength && + pageOffset + pageLength >= endOffset) { + // We read the entire interval without finding timestamps for all + // active tracks. We know the interval start offset is before the seek + // target, and the interval end is after the seek target, and we can't + // terminate inside the interval, so we terminate the seek at the + // start of the interval. + interval = 0; + break; + } + + // We should backoff; cause the guess to back off from the end, so + // that we've got more room to capture. + mustBackoff = true; + continue; + } + + // We've found appropriate time stamps here. Proceed to bisect + // the search space. + granuleTime = aType == TrackInfo::kAudioTrack ? audioTime : videoTime; + MOZ_ASSERT(granuleTime > 0, "Must get a granuletime"); + break; + } // End of "until we determine time at guess offset" loop. + + if (interval == 0) { + // Seek termination condition; we've found the page boundary of the + // last page before the target, and the first page after the target. + SEEK_LOG(LogLevel::Debug, + ("Terminating seek at offset=%lld", startOffset)); + MOZ_ASSERT(startTime < aTarget, + "Start time must always be less than target"); + res = Resource(aType)->Seek(nsISeekableStream::NS_SEEK_SET, startOffset); + NS_ENSURE_SUCCESS(res, res); + if (NS_FAILED(Reset(aType))) { + return NS_ERROR_FAILURE; + } + break; + } + + SEEK_LOG(LogLevel::Debug, + ("Time at offset %lld is %lld", guess, granuleTime)); + if (granuleTime < seekTarget && granuleTime > seekLowerBound) { + // We're within the fuzzy region in which we want to terminate the search. + res = Resource(aType)->Seek(nsISeekableStream::NS_SEEK_SET, pageOffset); + NS_ENSURE_SUCCESS(res, res); + if (NS_FAILED(Reset(aType))) { + return NS_ERROR_FAILURE; + } + SEEK_LOG(LogLevel::Debug, + ("Terminating seek at offset=%lld", pageOffset)); + break; + } + + if (granuleTime >= seekTarget) { + // We've landed after the seek target. + MOZ_ASSERT(pageOffset < endOffset, "offset_end must decrease"); + endOffset = pageOffset; + endTime = granuleTime; + } else if (granuleTime < seekTarget) { + // Landed before seek target. + MOZ_ASSERT(pageOffset >= startOffset + startLength, + "Bisection point should be at or after end of first page in " + "interval"); + startOffset = pageOffset; + startLength = pageLength; + startTime = granuleTime; + } + MOZ_ASSERT(startTime <= seekTarget, "Must be before seek target"); + MOZ_ASSERT(endTime >= seekTarget, "End must be after seek target"); + } + + (void)hops; + SEEK_LOG(LogLevel::Debug, ("Seek complete in %d bisections.", hops)); + + return NS_OK; +} + +#undef OGG_DEBUG +#undef SEEK_LOG +#undef CopyAndVerifyOrFail +} // namespace mozilla |