Source code

Revision control

Other Tools

/* -*- 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 int64_t OGG_SEEK_OPUS_PREROLL = 80 * USECS_PER_MS;
static Atomic<uint32_t> sStreamSourceID(0u);
OggDemuxer::nsAutoOggSyncState::nsAutoOggSyncState(rlbox_sandbox_ogg& aSandbox)
: mSandbox(aSandbox) {
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() {
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
// Firefox preloads the library externally to ensure we won't be stopped
// by the content sandbox
const bool external_loads_exist = true;
// See Bug 1606981: In some environments allowing stdio in the wasm sandbox
// fails as the I/O redirection involves querying meta-data of file
// descriptors. This querying fails in some environments.
const bool allow_stdio = false;
sandbox->create_sandbox(mozilla::ipc::GetSandboxedOggPath().get(),
external_loads_exist, allow_stdio);
#else
sandbox->create_sandbox();
#endif
return sandbox;
}
void OggDemuxer::SandboxDestroy::operator()(rlbox_sandbox_ogg* sandbox) {
sandbox->destroy_sandbox();
delete sandbox;
}
// Return the corresponding category in aKind based on the following specs.
// work/multipage/embedded-content.html#dom-audiotrack-kind) &
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),
mVideoOggState(aResource, *mSandbox),
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);
if (HasAudio() || HasVideo()) {
// If we were able to initialize our decoders, report whether we encountered
// a chained stream or not.
bool isChained = mIsChained;
void* ptr = this;
nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction(
"OggDemuxer::~OggDemuxer", [ptr, isChained]() -> void {
// We can't use OGG_DEBUG here because it implicitly refers to `this`,
// which we can't capture in this runnable.
MOZ_LOG(gMediaDemuxerLog, mozilla::LogLevel::Debug,
("OggDemuxer(%p)::%s: Reporting telemetry "
"MEDIA_OGG_LOADED_IS_CHAINED=%d",
ptr, __func__, isChained));
Telemetry::Accumulate(
Telemetry::HistogramID::MEDIA_OGG_LOADED_IS_CHAINED, isChained);
});
SchedulerGroup::Dispatch(TaskCategory::Other, task.forget());
}
}
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() {
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.
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 (auto iter = tags->Iter(); !iter.Done(); iter.Next()) {
aInfo->mTags.AppendElement(MetadataTag(iter.Key(), iter.Data()));
}
}
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;
}
int serial = 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* codecState =
OggCodecState::Create(mSandbox.get(), page.to_opaque(), serial);
mCodecStore.Add(serial, codecState);
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;
}
int serial =
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, codecState.release());
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(
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);
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
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
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);
}
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 =
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); },
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) {