/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim:set ts=2 sw=2 sts=2 et cindent: */ /* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Mozilla code. * * The Initial Developer of the Original Code is the Mozilla Corporation. * Portions created by the Initial Developer are Copyright (C) 2007 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Chris Double * Chris Pearce * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ #include "nsError.h" #include "nsBuiltinDecoderStateMachine.h" #include "nsBuiltinDecoder.h" #include "nsOggReader.h" #include "VideoUtils.h" #include "theora/theoradec.h" #include "nsTimeRanges.h" #include "mozilla/TimeStamp.h" using namespace mozilla; // Un-comment to enable logging of seek bisections. //#define SEEK_LOGGING #ifdef PR_LOGGING extern PRLogModuleInfo* gBuiltinDecoderLog; #define LOG(type, msg) PR_LOG(gBuiltinDecoderLog, type, msg) #ifdef SEEK_LOGGING #define SEEK_LOG(type, msg) PR_LOG(gBuiltinDecoderLog, type, msg) #else #define SEEK_LOG(type, msg) #endif #else #define LOG(type, msg) #define SEEK_LOG(type, msg) #endif // If we don't have a Theora video stream, then during seeking, if a seek // target is less than SEEK_DECODE_MARGIN ahead of the current playback // position, we'll just decode forwards rather than performing a bisection // search. If we have Theora video we use the maximum keyframe interval as // this value, rather than SEEK_DECODE_MARGIN. This makes small seeks faster. #define SEEK_DECODE_MARGIN 2000000 // 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 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. #define SEEK_FUZZ_USECS 500000 enum PageSyncResult { PAGE_SYNC_ERROR = 1, PAGE_SYNC_END_OF_RANGE= 2, PAGE_SYNC_OK = 3 }; // Reads a page from the media stream. static PageSyncResult PageSync(nsMediaStream* aStream, ogg_sync_state* aState, PRBool aCachedDataOnly, PRInt64 aOffset, PRInt64 aEndOffset, ogg_page* aPage, int& aSkippedBytes); // Chunk size to read when reading Ogg files. Average Ogg page length // is about 4300 bytes, so we read the file in chunks larger than that. static const int PAGE_STEP = 8192; nsOggReader::nsOggReader(nsBuiltinDecoder* aDecoder) : nsBuiltinDecoderReader(aDecoder), mTheoraState(nsnull), mVorbisState(nsnull), mSkeletonState(nsnull), mVorbisSerial(0), mTheoraSerial(0), mPageOffset(0), mTheoraGranulepos(-1), mVorbisGranulepos(-1), mDataOffset(0) { MOZ_COUNT_CTOR(nsOggReader); } nsOggReader::~nsOggReader() { ogg_sync_clear(&mOggState); MOZ_COUNT_DTOR(nsOggReader); } nsresult nsOggReader::Init(nsBuiltinDecoderReader* aCloneDonor) { PRBool init = mCodecStates.Init(); NS_ASSERTION(init, "Failed to initialize mCodecStates"); if (!init) { return NS_ERROR_FAILURE; } int ret = ogg_sync_init(&mOggState); NS_ENSURE_TRUE(ret == 0, NS_ERROR_FAILURE); return NS_OK; } nsresult nsOggReader::ResetDecode() { nsresult res = NS_OK; // Clear the Theora/Vorbis granulepos capture status, so that the next // decode calls recaptures the granulepos. mTheoraGranulepos = -1; mVorbisGranulepos = -1; if (NS_FAILED(nsBuiltinDecoderReader::ResetDecode())) { res = NS_ERROR_FAILURE; } { MonitorAutoEnter mon(mMonitor); // Discard any previously buffered packets/pages. ogg_sync_reset(&mOggState); if (mVorbisState && NS_FAILED(mVorbisState->Reset())) { res = NS_ERROR_FAILURE; } if (mTheoraState && NS_FAILED(mTheoraState->Reset())) { res = NS_ERROR_FAILURE; } } return res; } // Returns PR_TRUE when all bitstreams in aBitstreams array have finished // reading their headers. static PRBool DoneReadingHeaders(nsTArray& aBitstreams) { for (PRUint32 i = 0; i < aBitstreams .Length(); i++) { if (!aBitstreams [i]->DoneReadingHeaders()) { return PR_FALSE; } } return PR_TRUE; } nsresult nsOggReader::ReadMetadata(nsVideoInfo* aInfo) { NS_ASSERTION(mDecoder->OnStateMachineThread(), "Should be on play state machine thread."); MonitorAutoEnter mon(mMonitor); // 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. ogg_page page; PRInt64 pageOffset; nsAutoTArray bitstreams; PRBool readAllBOS = PR_FALSE; mDataOffset = 0; while (PR_TRUE) { if (readAllBOS && DoneReadingHeaders(bitstreams)) { if (mDataOffset == 0) { // We've previously found the start of the first non-header packet. mDataOffset = mPageOffset; } break; } pageOffset = ReadOggPage(&page); if (pageOffset == -1) { // Some kind of error... break; } int ret = 0; int serial = ogg_page_serialno(&page); nsOggCodecState* codecState = 0; if (ogg_page_bos(&page)) { NS_ASSERTION(!readAllBOS, "We shouldn't encounter another BOS page"); codecState = nsOggCodecState::Create(&page); #ifdef DEBUG PRBool r = #endif mCodecStates.Put(serial, codecState); NS_ASSERTION(r, "Failed to insert into mCodecStates"); bitstreams.AppendElement(codecState); mKnownStreams.AppendElement(serial); if (codecState && codecState->GetType() == nsOggCodecState::TYPE_VORBIS && !mVorbisState) { // First Vorbis bitstream, we'll play this one. Subsequent Vorbis // bitstreams will be ignored. mVorbisState = static_cast(codecState); } if (codecState && codecState->GetType() == nsOggCodecState::TYPE_THEORA && !mTheoraState) { // First Theora bitstream, we'll play this one. Subsequent Theora // bitstreams will be ignored. mTheoraState = static_cast(codecState); } if (codecState && codecState->GetType() == nsOggCodecState::TYPE_SKELETON && !mSkeletonState) { mSkeletonState = static_cast(codecState); } } else { // We've encountered the 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 = PR_TRUE; } mCodecStates.Get(serial, &codecState); NS_ENSURE_TRUE(codecState, NS_ERROR_FAILURE); // Add a complete page to the bitstream ret = ogg_stream_pagein(&codecState->mState, &page); NS_ENSURE_TRUE(ret == 0, NS_ERROR_FAILURE); // Process all available header packets in the stream. ogg_packet packet; if (codecState->DoneReadingHeaders() && mDataOffset == 0) { // Stream has read all header packets, but now there's more data in // (presumably) a non-header page, we must have finished header packets. // This can happen in incorrectly chopped streams. mDataOffset = pageOffset; continue; } while (!codecState->DoneReadingHeaders() && (ret = ogg_stream_packetout(&codecState->mState, &packet)) != 0) { if (ret == -1) { // Sync lost, we've probably encountered the continuation of a packet // in a chopped video. continue; } // A packet is available. If it is not a header packet we'll break. // If it is a header packet, process it as normal. codecState->DecodeHeader(&packet); } if (ogg_stream_packetpeek(&codecState->mState, &packet) != 0 && mDataOffset == 0) { // We're finished reading headers for this bitstream, but there's still // packets in the bitstream to read. The bitstream is probably poorly // muxed, and includes the last header packet on a page with non-header // packets. We need to ensure that this is the media start page offset. mDataOffset = pageOffset; } } // Deactivate any non-primary bitstreams. for (PRUint32 i = 0; i < bitstreams.Length(); i++) { nsOggCodecState* s = bitstreams[i]; if (s != mVorbisState && s != mTheoraState && s != mSkeletonState) { s->Deactivate(); } } // Initialize the first Theora and Vorbis bitstreams. According to the // Theora spec these can be considered the 'primary' bitstreams for playback. // Extract the metadata needed from these streams. // Set a default callback period for if we have no video data if (mTheoraState && mTheoraState->Init()) { gfxIntSize sz(mTheoraState->mInfo.pic_width, mTheoraState->mInfo.pic_height); mDecoder->SetVideoData(sz, mTheoraState->mPixelAspectRatio, nsnull, TimeStamp::Now()); } if (mVorbisState) { mVorbisState->Init(); } if (!HasAudio() && !HasVideo() && mSkeletonState) { // 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. mSkeletonState->Deactivate(); } mInfo.mHasAudio = HasAudio(); mInfo.mHasVideo = HasVideo(); if (HasAudio()) { mInfo.mAudioRate = mVorbisState->mInfo.rate; mInfo.mAudioChannels = mVorbisState->mInfo.channels; } if (HasVideo()) { mInfo.mPixelAspectRatio = mTheoraState->mPixelAspectRatio; mInfo.mPicture = nsIntRect(mTheoraState->mInfo.pic_x, mTheoraState->mInfo.pic_y, mTheoraState->mInfo.pic_width, mTheoraState->mInfo.pic_height); mInfo.mFrame = nsIntSize(mTheoraState->mInfo.frame_width, mTheoraState->mInfo.frame_height); mInfo.mDisplay = nsIntSize(mInfo.mPicture.width, mInfo.mPicture.height); } if (mSkeletonState && mSkeletonState->HasIndex()) { // Extract the duration info out of the index, so we don't need to seek to // the end of stream to get it. nsAutoTArray tracks; if (HasVideo()) { tracks.AppendElement(mTheoraState->mSerial); } if (HasAudio()) { tracks.AppendElement(mVorbisState->mSerial); } PRInt64 duration = 0; if (NS_SUCCEEDED(mSkeletonState->GetDuration(tracks, duration))) { MonitorAutoExit exitReaderMon(mMonitor); MonitorAutoEnter decoderMon(mDecoder->GetMonitor()); mDecoder->GetStateMachine()->SetDuration(duration); LOG(PR_LOG_DEBUG, ("Got duration from Skeleton index %lld", duration)); } } // Copy Vorbis and Theora info data for time computations on other threads. if (mVorbisState) { memcpy(&mVorbisInfo, &mVorbisState->mInfo, sizeof(mVorbisInfo)); mVorbisInfo.codec_setup = NULL; mVorbisSerial = mVorbisState->mSerial; } if (mTheoraState) { memcpy(&mTheoraInfo, &mTheoraState->mInfo, sizeof(mTheoraInfo)); mTheoraSerial = mTheoraState->mSerial; } *aInfo = mInfo; LOG(PR_LOG_DEBUG, ("Done loading headers, data offset %lld", mDataOffset)); return NS_OK; } nsresult nsOggReader::DecodeVorbis(nsTArray >& aChunks, ogg_packet* aPacket) { // Successfully read a packet. if (vorbis_synthesis(&mVorbisState->mBlock, aPacket) != 0) { return NS_ERROR_FAILURE; } if (vorbis_synthesis_blockin(&mVorbisState->mDsp, &mVorbisState->mBlock) != 0) { return NS_ERROR_FAILURE; } VorbisPCMValue** pcm = 0; PRInt32 samples = 0; PRUint32 channels = mVorbisState->mInfo.channels; while ((samples = vorbis_synthesis_pcmout(&mVorbisState->mDsp, &pcm)) > 0) { SoundDataValue* buffer = new SoundDataValue[samples * channels]; for (PRUint32 j = 0; j < channels; ++j) { VorbisPCMValue* channel = pcm[j]; for (PRUint32 i = 0; i < PRUint32(samples); ++i) { buffer[i*channels + j] = MOZ_CONVERT_VORBIS_SAMPLE(channel[i]); } } PRInt64 duration = mVorbisState->Time((PRInt64)samples); PRInt64 startTime = mVorbisState->Time(mVorbisGranulepos); SoundData* s = new SoundData(mPageOffset, startTime, duration, samples, buffer, channels); if (mVorbisGranulepos != -1) { mVorbisGranulepos += samples; } if (!aChunks.AppendElement(s)) { delete s; } if (vorbis_synthesis_read(&mVorbisState->mDsp, samples) != 0) { return NS_ERROR_FAILURE; } } return NS_OK; } PRBool nsOggReader::DecodeAudioData() { MonitorAutoEnter mon(mMonitor); NS_ASSERTION(mDecoder->OnStateMachineThread() || mDecoder->OnDecodeThread(), "Should be on playback or decode thread."); NS_ASSERTION(mVorbisState!=0, "Need Vorbis state to decode audio"); ogg_packet packet; packet.granulepos = -1; PRBool endOfStream = PR_FALSE; nsAutoTArray, 64> chunks; if (mVorbisGranulepos == -1) { // Not captured Vorbis granulepos, read up until we get a granulepos, and // back propagate the granulepos. // We buffer the packets' pcm samples until we reach a packet with a granulepos. // This will be the last packet in a page. Then using that granulepos to // calculate the packet's end time, we calculate all the packets' start times by // subtracting their durations. // Ensure we've got Vorbis packets; read one more Vorbis page if necessary. while (packet.granulepos <= 0 && !endOfStream) { if (!ReadOggPacket(mVorbisState, &packet)) { endOfStream = PR_TRUE; break; } if (packet.e_o_s != 0) { // This packet marks the logical end of the Vorbis bitstream. It may // still contain sound samples, so we must still decode it. endOfStream = PR_TRUE; } if (NS_FAILED(DecodeVorbis(chunks, &packet))) { NS_WARNING("Failed to decode Vorbis packet"); } } if (packet.granulepos > 0) { // Successfully read up to a non -1 granulepos. // Calculate the timestamps of the sound samples. PRInt64 granulepos = packet.granulepos; // Represents end time of last sample. mVorbisGranulepos = packet.granulepos; for (int i = chunks.Length() - 1; i >= 0; --i) { SoundData* s = chunks[i]; PRInt64 startGranule = granulepos - s->mSamples; s->mTime = mVorbisState->Time(startGranule); granulepos = startGranule; } } } else { // We have already captured the granulepos. The next packet's granulepos // is its number of samples, plus the previous granulepos. if (!ReadOggPacket(mVorbisState, &packet)) { endOfStream = PR_TRUE; } else { // Successfully read a packet from the file. Decode it. endOfStream = packet.e_o_s != 0; // Try to decode any packet we've read. if (NS_FAILED(DecodeVorbis(chunks, &packet))) { NS_WARNING("Failed to decode Vorbis packet"); } if (packet.granulepos != -1 && packet.granulepos != mVorbisGranulepos) { // If the packet's granulepos doesn't match our running sample total, // it's likely the bitstream has been damaged somehow, or perhaps // oggz-chopped. Just assume the packet's granulepos is correct... mVorbisGranulepos = packet.granulepos; } } } // We've successfully decoded some sound chunks. Push them onto the audio // queue. for (PRUint32 i = 0; i < chunks.Length(); ++i) { mAudioQueue.Push(chunks[i].forget()); } if (endOfStream) { // We've encountered an end of bitstream packet, or we've hit the end of // file while trying to decode, so inform the audio queue that there'll // be no more samples. mAudioQueue.Finish(); return PR_FALSE; } return PR_TRUE; } // Returns 1 if the Theora info struct is decoding a media of Theora // verion (maj,min,sub) or later, otherwise returns 0. static int TheoraVersion(th_info* info, unsigned char maj, unsigned char min, unsigned char sub) { ogg_uint32_t ver = (maj << 16) + (min << 8) + sub; ogg_uint32_t th_ver = (info->version_major << 16) + (info->version_minor << 8) + info->version_subminor; return (th_ver >= ver) ? 1 : 0; } #ifdef DEBUG // Ensures that all the VideoData in aFrames array are stored in increasing // order by timestamp. Used in assertions in debug builds. static PRBool AllFrameTimesIncrease(nsTArray >& aFrames) { PRInt64 prevTime = -1; PRInt64 prevGranulepos = -1; for (PRUint32 i = 0; i < aFrames.Length(); i++) { VideoData* f = aFrames[i]; if (f->mTime < prevTime) { return PR_FALSE; } prevTime = f->mTime; prevGranulepos = f->mTimecode; } return PR_TRUE; } #endif nsresult nsOggReader::DecodeTheora(nsTArray >& aFrames, ogg_packet* aPacket) { int ret = th_decode_packetin(mTheoraState->mCtx, aPacket, 0); if (ret != 0 && ret != TH_DUPFRAME) { return NS_ERROR_FAILURE; } PRInt64 time = mTheoraState->StartTime(aPacket->granulepos); PRInt64 endTime = mTheoraState->Time(aPacket->granulepos); if (ret == TH_DUPFRAME) { VideoData* v = VideoData::CreateDuplicate(mPageOffset, time, endTime, aPacket->granulepos); if (!aFrames.AppendElement(v)) { delete v; } } else if (ret == 0) { th_ycbcr_buffer buffer; ret = th_decode_ycbcr_out(mTheoraState->mCtx, buffer); NS_ASSERTION(ret == 0, "th_decode_ycbcr_out failed"); PRBool isKeyframe = th_packet_iskeyframe(aPacket) == 1; VideoData::YCbCrBuffer b; for (PRUint32 i=0; i < 3; ++i) { b.mPlanes[i].mData = buffer[i].data; b.mPlanes[i].mHeight = buffer[i].height; b.mPlanes[i].mWidth = buffer[i].width; b.mPlanes[i].mStride = buffer[i].stride; } // Need the monitor to be held to be able to use mInfo. This // is held by our caller. mMonitor.AssertCurrentThreadIn(); VideoData *v = VideoData::Create(mInfo, mDecoder->GetImageContainer(), mPageOffset, time, endTime, b, isKeyframe, aPacket->granulepos); if (!v) { // There may be other reasons for this error, but for // simplicity just assume the worst case: out of memory. NS_WARNING("Failed to allocate memory for video frame"); return NS_ERROR_OUT_OF_MEMORY; } if (!aFrames.AppendElement(v)) { delete v; } } return NS_OK; } PRBool nsOggReader::DecodeVideoFrame(PRBool &aKeyframeSkip, PRInt64 aTimeThreshold) { MonitorAutoEnter mon(mMonitor); NS_ASSERTION(mDecoder->OnStateMachineThread() || mDecoder->OnDecodeThread(), "Should be on state machine or AV thread."); // Record number of frames decoded and parsed. Automatically update the // stats counters using the AutoNotifyDecoded stack-based class. PRUint32 parsed = 0, decoded = 0; nsMediaDecoder::AutoNotifyDecoded autoNotify(mDecoder, parsed, decoded); // We chose to keep track of the Theora granulepos ourselves, rather than // rely on th_decode_packetin() to do it for us. This is because // th_decode_packetin() simply works by incrementing a counter every time // it's called, so if we drop frames and don't call it, subsequent granulepos // will be wrong. Whenever we read a packet which has a granulepos, we use // its granulepos, otherwise we increment the previous packet's granulepos. nsAutoTArray, 8> frames; ogg_packet packet; PRBool endOfStream = PR_FALSE; if (mTheoraGranulepos == -1) { // We've not read a Theora packet with a granulepos, so we don't know what // timestamp to assign to Theora frames we decode. This will only happen // the first time we read, or after a seek. We must read and buffer up to // the first Theora packet with a granulepos, and back-propagate its // granulepos to calculate the buffered frames' granulepos. do { if (!ReadOggPacket(mTheoraState, &packet)) { // Failed to read another page, must be the end of file. We can't have // already encountered an end of bitstream packet, else we wouldn't be // here, so this bitstream must be missing its end of stream packet, or // is otherwise corrupt (oggz-chop can output files like this). Inform // the queue that there will be no more frames. mVideoQueue.Finish(); return PR_FALSE; } parsed++; if (packet.granulepos > 0) { // We've found a packet with a granulepos, we can now determine the // buffered packet's timestamps, as well as the timestamps for any // packets we read subsequently. mTheoraGranulepos = packet.granulepos; } if (DecodeTheora(frames, &packet) == NS_ERROR_OUT_OF_MEMORY) { NS_WARNING("Theora decode memory allocation failure!"); return PR_FALSE; } } while (packet.granulepos <= 0 && !endOfStream); if (packet.granulepos > 0) { // Reconstruct the granulepos (and thus timestamps) of the decoded // frames. Granulepos are stored as ((keyframe<mInfo.keyframe_granule_shift; ogg_int64_t version_3_2_1 = TheoraVersion(&mTheoraState->mInfo,3,2,1); ogg_int64_t lastFrame = th_granule_frame(mTheoraState->mCtx, packet.granulepos) + version_3_2_1; ogg_int64_t firstFrame = lastFrame - frames.Length() + 1; // Until we encounter a keyframe, we'll assume that the "keyframe" // segment of the granulepos is the first frame, or if that causes // the "offset" segment to overflow, we assume the required // keyframe is maximumally offset. Until we encounter a keyframe // the granulepos will probably be wrong, but we can't decode the // frame anyway (since we don't have its keyframe) so it doesn't really // matter. ogg_int64_t keyframe = packet.granulepos >> shift; // The lastFrame, firstFrame, keyframe variables, as well as the frame // variable in the loop below, store the frame number for Theora // version >= 3.2.1 streams, and store the frame index for Theora // version < 3.2.1 streams. for (PRUint32 i = 0; i < frames.Length() - 1; ++i) { ogg_int64_t frame = firstFrame + i; ogg_int64_t granulepos; if (frames[i]->mKeyframe) { granulepos = frame << shift; keyframe = frame; } else if (frame >= keyframe && frame - keyframe < ((ogg_int64_t)1 << shift)) { // (frame - keyframe) won't overflow the "offset" segment of the // granulepos, so it's safe to calculate the granulepos. granulepos = (keyframe << shift) + (frame - keyframe); } else { // (frame - keyframeno) will overflow the "offset" segment of the // granulepos, so we take "keyframe" to be the max possible offset // frame instead. ogg_int64_t k = NS_MAX(frame - (((ogg_int64_t)1 << shift) - 1), version_3_2_1); granulepos = (k << shift) + (frame - k); } // Theora 3.2.1+ granulepos store frame number [1..N], so granulepos // should be > 0. // Theora 3.2.0 granulepos store the frame index [0..(N-1)], so // granulepos should be >= 0. NS_ASSERTION(granulepos >= version_3_2_1, "Invalid granulepos for Theora version"); // Check that the frame's granule number is one more than the // previous frame's. NS_ASSERTION(i == 0 || th_granule_frame(mTheoraState->mCtx, granulepos) == th_granule_frame(mTheoraState->mCtx, frames[i-1]->mTimecode) + 1, "Granulepos calculation is incorrect!"); frames[i]->mTime = mTheoraState->StartTime(granulepos); frames[i]->mEndTime = mTheoraState->Time(granulepos); NS_ASSERTION(frames[i]->mEndTime >= frames[i]->mTime, "Frame must start before it ends."); frames[i]->mTimecode = granulepos; } NS_ASSERTION(AllFrameTimesIncrease(frames), "All frames must have granulepos"); // Check that the second to last frame's granule number is one less than // the last frame's (the known granule number). If not our granulepos // recovery missed a beat. NS_ASSERTION(frames.Length() < 2 || th_granule_frame(mTheoraState->mCtx, frames[frames.Length()-2]->mTimecode) + 1 == th_granule_frame(mTheoraState->mCtx, packet.granulepos), "Granulepos recovery should catch up with packet.granulepos!"); } } else { NS_ASSERTION(mTheoraGranulepos > 0, "We must Theora granulepos!"); if (!ReadOggPacket(mTheoraState, &packet)) { // Failed to read from file, so EOF or other premature failure. // Inform the queue that there will be no more frames. mVideoQueue.Finish(); return PR_FALSE; } parsed++; endOfStream = packet.e_o_s != 0; // Maintain the Theora granulepos. We must do this even if we drop frames, // otherwise our clock will be wrong after we've skipped frames. if (packet.granulepos != -1) { // Incoming packet has a granulepos, use that as it's granulepos. mTheoraGranulepos = packet.granulepos; } else { // Increment the previous Theora granulepos. PRInt64 granulepos = 0; int shift = mTheoraState->mInfo.keyframe_granule_shift; // Theora 3.2.1+ bitstreams granulepos store frame number; [1..N] // Theora 3.2.0 bitstreams store the frame index; [0..(N-1)] if (!th_packet_iskeyframe(&packet)) { granulepos = mTheoraGranulepos + 1; } else { ogg_int64_t frameindex = th_granule_frame(mTheoraState->mCtx, mTheoraGranulepos); ogg_int64_t granule = frameindex + TheoraVersion(&mTheoraState->mInfo,3,2,1) + 1; NS_ASSERTION(granule > 0, "Must have positive granulepos"); granulepos = granule << shift; } NS_ASSERTION(th_granule_frame(mTheoraState->mCtx, mTheoraGranulepos) + 1 == th_granule_frame(mTheoraState->mCtx, granulepos), "Frame number must increment by 1"); packet.granulepos = mTheoraGranulepos = granulepos; } PRInt64 time = mTheoraState->StartTime(mTheoraGranulepos); NS_ASSERTION(packet.granulepos != -1, "Must know packet granulepos"); if (!aKeyframeSkip || (th_packet_iskeyframe(&packet) == 1 && time >= aTimeThreshold)) { if (DecodeTheora(frames, &packet) == NS_ERROR_OUT_OF_MEMORY) { NS_WARNING("Theora decode memory allocation failure"); return PR_FALSE; } } } // Push decoded data into the video frame queue. for (PRUint32 i = 0; i < frames.Length(); i++) { nsAutoPtr data(frames[i].forget()); // Don't use the frame if it's outside the bounds of the presentation // start time in the skeleton track. if (!mSkeletonState || mSkeletonState->IsPresentable(data->mTime)) { if (aKeyframeSkip && data->mKeyframe) { aKeyframeSkip = PR_FALSE; } if (!aKeyframeSkip && data->mEndTime >= aTimeThreshold) { mVideoQueue.Push(data.forget()); decoded++; } } } if (endOfStream) { // We've encountered an end of bitstream packet. Inform the queue that // there will be no more frames. mVideoQueue.Finish(); } return !endOfStream; } PRInt64 nsOggReader::ReadOggPage(ogg_page* aPage) { NS_ASSERTION(mDecoder->OnStateMachineThread() || mDecoder->OnDecodeThread(), "Should be on play state machine or decode thread."); mMonitor.AssertCurrentThreadIn(); int ret = 0; while((ret = ogg_sync_pageseek(&mOggState, aPage)) <= 0) { if (ret < 0) { // Lost page sync, have to skip up to next page. mPageOffset += -ret; continue; } // Returns a buffer that can be written too // with the given size. This buffer is stored // in the ogg synchronisation structure. char* buffer = ogg_sync_buffer(&mOggState, 4096); NS_ASSERTION(buffer, "ogg_sync_buffer failed"); // Read from the stream into the buffer PRUint32 bytesRead = 0; nsresult rv = mDecoder->GetCurrentStream()->Read(buffer, 4096, &bytesRead); if (NS_FAILED(rv) || (bytesRead == 0 && ret == 0)) { // End of file. return -1; } mDecoder->NotifyBytesConsumed(bytesRead); // Update the synchronisation layer with the number // of bytes written to the buffer ret = ogg_sync_wrote(&mOggState, bytesRead); NS_ENSURE_TRUE(ret == 0, -1); } PRInt64 offset = mPageOffset; mPageOffset += aPage->header_len + aPage->body_len; return offset; } PRBool nsOggReader::ReadOggPacket(nsOggCodecState* aCodecState, ogg_packet* aPacket) { NS_ASSERTION(mDecoder->OnStateMachineThread() || mDecoder->OnDecodeThread(), "Should be on play state machine or decode thread."); mMonitor.AssertCurrentThreadIn(); if (!aCodecState || !aCodecState->mActive) { return PR_FALSE; } int ret = 0; while ((ret = ogg_stream_packetout(&aCodecState->mState, aPacket)) != 1) { ogg_page page; if (aCodecState->PageInFromBuffer()) { // The codec state has inserted a previously buffered page into its // ogg_stream_state, no need to read a page from the channel. continue; } // The codec state does not have any buffered pages, so try to read another // page from the channel. if (ReadOggPage(&page) == -1) { return PR_FALSE; } PRUint32 serial = ogg_page_serialno(&page); nsOggCodecState* codecState = nsnull; mCodecStates.Get(serial, &codecState); if (serial == aCodecState->mSerial) { // This page is from our target bitstream, insert it into the // codec state's ogg_stream_state so we can read a packet. ret = ogg_stream_pagein(&codecState->mState, &page); NS_ENSURE_TRUE(ret == 0, PR_FALSE); } else if (codecState && codecState->mActive) { // Page is for another active bitstream, add the page to its codec // state's buffer for later consumption when that stream next tries // to read a packet. codecState->AddToBuffer(&page); } } return PR_TRUE; } // Returns an ogg page's checksum. static ogg_uint32_t GetChecksum(ogg_page* page) { if (page == 0 || page->header == 0 || page->header_len < 25) { return 0; } const unsigned char* p = page->header + 22; PRUint32 c = p[0] + (p[1] << 8) + (p[2] << 16) + (p[3] << 24); return c; } VideoData* nsOggReader::FindStartTime(PRInt64 aOffset, PRInt64& aOutStartTime) { NS_ASSERTION(mDecoder->OnStateMachineThread(), "Should be on state machine thread."); nsMediaStream* stream = mDecoder->GetCurrentStream(); NS_ENSURE_TRUE(stream != nsnull, nsnull); // Ensure aOffset is after mDataOffset, the offset of the first non-header page. // This prevents us from trying to parse header pages as content pages. NS_ASSERTION(mDataOffset > 0, "Must know mDataOffset by now"); PRInt64 offset = NS_MAX(mDataOffset, aOffset); nsresult res = stream->Seek(nsISeekableStream::NS_SEEK_SET, offset); NS_ENSURE_SUCCESS(res, nsnull); return nsBuiltinDecoderReader::FindStartTime(offset, aOutStartTime); } PRInt64 nsOggReader::FindEndTime(PRInt64 aEndOffset) { MonitorAutoEnter mon(mMonitor); NS_ASSERTION(mDecoder->OnStateMachineThread(), "Should be on state machine thread."); NS_ASSERTION(mDataOffset > 0, "Should have offset of first non-header page"); PRInt64 offset = NS_MAX(mDataOffset, aEndOffset); PRInt64 endTime = FindEndTime(mDataOffset, offset, PR_FALSE, &mOggState); // Reset read head to start of media data. nsMediaStream* stream = mDecoder->GetCurrentStream(); NS_ENSURE_TRUE(stream != nsnull, -1); nsresult res = stream->Seek(nsISeekableStream::NS_SEEK_SET, mDataOffset); NS_ENSURE_SUCCESS(res, -1); return endTime; } PRInt64 nsOggReader::FindEndTime(PRInt64 aStartOffset, PRInt64 aEndOffset, PRBool aCachedDataOnly, ogg_sync_state* aState) { nsMediaStream* stream = mDecoder->GetCurrentStream(); ogg_sync_reset(aState); // 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; PRInt64 readStartOffset = aEndOffset; PRInt64 readHead = aEndOffset; PRInt64 endTime = -1; PRUint32 checksumAfterSeek = 0; PRUint32 prevChecksumAfterSeek = 0; PRBool mustBackOff = PR_FALSE; while (PR_TRUE) { ogg_page page; int ret = ogg_sync_pageseek(aState, &page); if (ret == 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 = PR_FALSE; prevChecksumAfterSeek = checksumAfterSeek; checksumAfterSeek = 0; ogg_sync_reset(aState); readStartOffset = NS_MAX(static_cast(0), readStartOffset - step); readHead = NS_MAX(aStartOffset, readStartOffset); } PRInt64 limit = NS_MIN(static_cast(PR_UINT32_MAX), aEndOffset - readHead); limit = NS_MAX(static_cast(0), limit); limit = NS_MIN(limit, static_cast(step)); PRUint32 bytesToRead = static_cast(limit); PRUint32 bytesRead = 0; char* buffer = ogg_sync_buffer(aState, bytesToRead); NS_ASSERTION(buffer, "Must have buffer"); nsresult res; if (aCachedDataOnly) { res = stream->ReadFromCache(buffer, readHead, bytesToRead); NS_ENSURE_SUCCESS(res, -1); bytesRead = bytesToRead; } else { NS_ASSERTION(readHead < aEndOffset, "Stream pos must be before range end"); res = stream->Seek(nsISeekableStream::NS_SEEK_SET, readHead); NS_ENSURE_SUCCESS(res, -1); res = stream->Read(buffer, bytesToRead, &bytesRead); NS_ENSURE_SUCCESS(res, -1); } readHead += bytesRead; // Update the synchronisation layer with the number // of bytes written to the buffer ret = ogg_sync_wrote(aState, bytesRead); if (ret != 0) { endTime = -1; break; } continue; } if (ret < 0 || ogg_page_granulepos(&page) < 0) { continue; } PRUint32 checksum = GetChecksum(&page); 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 = PR_TRUE; continue; } PRInt64 granulepos = ogg_page_granulepos(&page); int serial = ogg_page_serialno(&page); nsOggCodecState* codecState = nsnull; mCodecStates.Get(serial, &codecState); 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... endTime = -1; break; } PRInt64 t = codecState->Time(granulepos); if (t != -1) { endTime = t; } } ogg_sync_reset(aState); return endTime; } nsresult nsOggReader::GetSeekRanges(nsTArray& aRanges) { NS_ASSERTION(mDecoder->OnStateMachineThread(), "Should be on state machine thread."); mMonitor.AssertCurrentThreadIn(); nsTArray cached; nsresult res = mDecoder->GetCurrentStream()->GetCachedRanges(cached); NS_ENSURE_SUCCESS(res, res); for (PRUint32 index = 0; index < cached.Length(); index++) { nsByteRange& range = cached[index]; PRInt64 startTime = -1; PRInt64 endTime = -1; if (NS_FAILED(ResetDecode())) { return NS_ERROR_FAILURE; } // Ensure the offsets are after the header pages. PRInt64 startOffset = NS_MAX(cached[index].mStart, mDataOffset); PRInt64 endOffset = NS_MAX(cached[index].mEnd, mDataOffset); FindStartTime(startOffset, startTime); if (startTime != -1 && ((endTime = FindEndTime(endOffset)) != -1)) { NS_ASSERTION(startTime < endTime, "Start time must be before end time"); aRanges.AppendElement(SeekRange(startOffset, endOffset, startTime, endTime)); } } if (NS_FAILED(ResetDecode())) { return NS_ERROR_FAILURE; } return NS_OK; } nsOggReader::SeekRange nsOggReader::SelectSeekRange(const nsTArray& ranges, PRInt64 aTarget, PRInt64 aStartTime, PRInt64 aEndTime, PRBool aExact) { NS_ASSERTION(mDecoder->OnStateMachineThread(), "Should be on state machine thread."); PRInt64 so = mDataOffset; PRInt64 eo = mDecoder->GetCurrentStream()->GetLength(); PRInt64 st = aStartTime; PRInt64 et = aEndTime; for (PRUint32 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]; } } return aExact ? SeekRange() : SeekRange(so, eo, st, et); } nsOggReader::IndexedSeekResult nsOggReader::RollbackIndexedSeek(PRInt64 aOffset) { mSkeletonState->Deactivate(); nsMediaStream* stream = mDecoder->GetCurrentStream(); NS_ENSURE_TRUE(stream != nsnull, SEEK_FATAL_ERROR); nsresult res = stream->Seek(nsISeekableStream::NS_SEEK_SET, aOffset); NS_ENSURE_SUCCESS(res, SEEK_FATAL_ERROR); return SEEK_INDEX_FAIL; } nsOggReader::IndexedSeekResult nsOggReader::SeekToKeyframeUsingIndex(PRInt64 aTarget) { nsMediaStream* stream = mDecoder->GetCurrentStream(); NS_ENSURE_TRUE(stream != nsnull, SEEK_FATAL_ERROR); if (!HasSkeleton() || !mSkeletonState->HasIndex()) { return SEEK_INDEX_FAIL; } // We have an index from the Skeleton track, try to use it to seek. nsAutoTArray tracks; if (HasVideo()) { tracks.AppendElement(mTheoraState->mSerial); } if (HasAudio()) { tracks.AppendElement(mVorbisState->mSerial); } nsSkeletonState::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 stream read cursor position so we can rollback on failure. PRInt64 tell = stream->Tell(); // Seek to the keypoint returned by the index. if (keyframe.mKeyPoint.mOffset > stream->GetLength() || keyframe.mKeyPoint.mOffset < 0) { // Index must be invalid. return RollbackIndexedSeek(tell); } LOG(PR_LOG_DEBUG, ("Seeking using index to keyframe at offset %lld\n", keyframe.mKeyPoint.mOffset)); nsresult res = stream->Seek(nsISeekableStream::NS_SEEK_SET, keyframe.mKeyPoint.mOffset); NS_ENSURE_SUCCESS(res, SEEK_FATAL_ERROR); mPageOffset = keyframe.mKeyPoint.mOffset; // We've moved the read set, so reset decode. res = ResetDecode(); 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. ogg_page page; int skippedBytes = 0; PageSyncResult syncres = PageSync(stream, &mOggState, PR_FALSE, mPageOffset, stream->GetLength(), &page, skippedBytes); NS_ENSURE_TRUE(syncres != PAGE_SYNC_ERROR, SEEK_FATAL_ERROR); if (syncres != PAGE_SYNC_OK || skippedBytes != 0) { LOG(PR_LOG_DEBUG, ("Indexed-seek failure: Ogg Skeleton Index is invalid " "or sync error after seek")); return RollbackIndexedSeek(tell); } PRUint32 serial = ogg_page_serialno(&page); 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(tell); } nsOggCodecState* codecState = nsnull; mCodecStates.Get(serial, &codecState); if (codecState && codecState->mActive && ogg_stream_pagein(&codecState->mState, &page) != 0) { // Couldn't insert page into the ogg stream, or somehow the stream // is no longer active. return RollbackIndexedSeek(tell); } mPageOffset = keyframe.mKeyPoint.mOffset + page.header_len + page.body_len; return SEEK_OK; } nsresult nsOggReader::SeekInBufferedRange(PRInt64 aTarget, PRInt64 aStartTime, PRInt64 aEndTime, const nsTArray& aRanges, const SeekRange& aRange) { LOG(PR_LOG_DEBUG, ("%p Seeking in buffered data to %lldms using bisection search", mDecoder, 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(aTarget, aRange, 0); if (NS_FAILED(res) || !HasVideo()) { return res; } // We have an active Theora bitstream. Decode the next Theora frame, and // extract its keyframe's time. PRBool eof; do { PRBool skip = PR_FALSE; eof = !DecodeVideoFrame(skip, 0); { MonitorAutoExit exitReaderMon(mMonitor); MonitorAutoEnter decoderMon(mDecoder->GetMonitor()); if (mDecoder->GetDecodeState() == nsBuiltinDecoderStateMachine::DECODER_STATE_SHUTDOWN) { return NS_ERROR_FAILURE; } } } while (!eof && mVideoQueue.GetSize() == 0); VideoData* video = mVideoQueue.PeekFront(); if (video && !video->mKeyframe) { // First decoded frame isn't a keyframe, seek back to previous keyframe, // otherwise we'll get visual artifacts. NS_ASSERTION(video->mTimecode != -1, "Must have a granulepos"); int shift = mTheoraState->mInfo.keyframe_granule_shift; PRInt64 keyframeGranulepos = (video->mTimecode >> shift) << shift; PRInt64 keyframeTime = mTheoraState->StartTime(keyframeGranulepos); SEEK_LOG(PR_LOG_DEBUG, ("Keyframe for %lld is at %lld, seeking back to it", video->mTime, keyframeTime)); SeekRange k = SelectSeekRange(aRanges, keyframeTime, aStartTime, aEndTime, PR_FALSE); res = SeekBisection(keyframeTime, k, SEEK_FUZZ_USECS); NS_ASSERTION(mTheoraGranulepos == -1, "SeekBisection must reset Theora decode"); NS_ASSERTION(mVorbisGranulepos == -1, "SeekBisection must reset Vorbis decode"); } return res; } PRBool nsOggReader::CanDecodeToTarget(PRInt64 aTarget, PRInt64 aCurrentTime) { // We can decode to the target if the target is no further than the // maximum keyframe offset ahead of the current playback position, if // we have video, or SEEK_DECODE_MARGIN if we don't have video. PRInt64 margin = HasVideo() ? mTheoraState->MaxKeyframeOffset() : SEEK_DECODE_MARGIN; return aTarget >= aCurrentTime && aTarget - aCurrentTime < margin; } nsresult nsOggReader::SeekInUnbuffered(PRInt64 aTarget, PRInt64 aStartTime, PRInt64 aEndTime, const nsTArray& aRanges) { LOG(PR_LOG_DEBUG, ("%p Seeking in unbuffered data to %lldms using bisection search", mDecoder, 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). PRInt64 keyframeOffsetMs = 0; if (HasVideo() && mTheoraState) { keyframeOffsetMs = mTheoraState->MaxKeyframeOffset(); } PRInt64 seekTarget = NS_MAX(aStartTime, aTarget - keyframeOffsetMs); // Minimize the bisection search space using the known timestamps from the // buffered ranges. SeekRange k = SelectSeekRange(aRanges, seekTarget, aStartTime, aEndTime, PR_FALSE); nsresult res = SeekBisection(seekTarget, k, SEEK_FUZZ_USECS); NS_ASSERTION(mTheoraGranulepos == -1, "SeekBisection must reset Theora decode"); NS_ASSERTION(mVorbisGranulepos == -1, "SeekBisection must reset Vorbis decode"); return res; } nsresult nsOggReader::Seek(PRInt64 aTarget, PRInt64 aStartTime, PRInt64 aEndTime, PRInt64 aCurrentTime) { MonitorAutoEnter mon(mMonitor); NS_ASSERTION(mDecoder->OnStateMachineThread(), "Should be on state machine thread."); LOG(PR_LOG_DEBUG, ("%p About to seek to %lldms", mDecoder, aTarget)); nsresult res; nsMediaStream* stream = mDecoder->GetCurrentStream(); NS_ENSURE_TRUE(stream != nsnull, NS_ERROR_FAILURE); if (aTarget == aStartTime) { // We've seeked to the media start. Just seek to the offset of the first // content page. res = stream->Seek(nsISeekableStream::NS_SEEK_SET, mDataOffset); NS_ENSURE_SUCCESS(res,res); mPageOffset = mDataOffset; res = ResetDecode(); NS_ENSURE_SUCCESS(res,res); NS_ASSERTION(aStartTime != -1, "mStartTime should be known"); { MonitorAutoExit exitReaderMon(mMonitor); MonitorAutoEnter decoderMon(mDecoder->GetMonitor()); mDecoder->UpdatePlaybackPosition(aStartTime); } } else if (CanDecodeToTarget(aTarget, aCurrentTime)) { LOG(PR_LOG_DEBUG, ("%p Seek target (%lld) is close to current time (%lld), " "will just decode to it", mDecoder, aCurrentTime, aTarget)); } else { IndexedSeekResult sres = SeekToKeyframeUsingIndex(aTarget); 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. nsAutoTArray ranges; res = GetSeekRanges(ranges); NS_ENSURE_SUCCESS(res,res); // Figure out if the seek target lies in a buffered range. SeekRange r = SelectSeekRange(ranges, aTarget, aStartTime, aEndTime, PR_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(aTarget, aStartTime, aEndTime, 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(aTarget, aStartTime, aEndTime, ranges); NS_ENSURE_SUCCESS(res,res); } } } // The decode position must now be either close to the seek target, or // we've seeked to before the keyframe before the seek target. Decode // forward to the seek target frame. return DecodeToTarget(aTarget); } // Reads a page from the media stream. static PageSyncResult PageSync(nsMediaStream* aStream, ogg_sync_state* aState, PRBool aCachedDataOnly, PRInt64 aOffset, PRInt64 aEndOffset, ogg_page* aPage, int& aSkippedBytes) { aSkippedBytes = 0; // Sync to the next page. int ret = 0; PRUint32 bytesRead = 0; PRInt64 readHead = aOffset; while (ret <= 0) { ret = ogg_sync_pageseek(aState, aPage); if (ret == 0) { char* buffer = ogg_sync_buffer(aState, PAGE_STEP); NS_ASSERTION(buffer, "Must have a buffer"); // Read from the file into the buffer PRUint32 bytesToRead = static_cast(NS_MIN(static_cast(PAGE_STEP), aEndOffset - readHead)); if (bytesToRead <= 0) { return PAGE_SYNC_END_OF_RANGE; } nsresult rv = NS_OK; if (aCachedDataOnly) { rv = aStream->ReadFromCache(buffer, readHead, bytesToRead); NS_ENSURE_SUCCESS(rv,PAGE_SYNC_ERROR); bytesRead = bytesToRead; } else { rv = aStream->Seek(nsISeekableStream::NS_SEEK_SET, readHead); NS_ENSURE_SUCCESS(rv,PAGE_SYNC_ERROR); rv = aStream->Read(buffer, static_cast(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 = ogg_sync_wrote(aState, bytesRead); NS_ENSURE_TRUE(ret == 0, PAGE_SYNC_ERROR); continue; } if (ret < 0) { NS_ASSERTION(aSkippedBytes >= 0, "Offset >= 0"); aSkippedBytes += -ret; NS_ASSERTION(aSkippedBytes >= 0, "Offset >= 0"); continue; } } return PAGE_SYNC_OK; } nsresult nsOggReader::SeekBisection(PRInt64 aTarget, const SeekRange& aRange, PRUint32 aFuzz) { NS_ASSERTION(mDecoder->OnStateMachineThread(), "Should be on state machine thread."); nsresult res; nsMediaStream* stream = mDecoder->GetCurrentStream(); if (aTarget == aRange.mTimeStart) { if (NS_FAILED(ResetDecode())) { return NS_ERROR_FAILURE; } res = stream->Seek(nsISeekableStream::NS_SEEK_SET, mDataOffset); NS_ENSURE_SUCCESS(res,res); mPageOffset = mDataOffset; 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; PRInt64 seekLowerBound = NS_MAX(static_cast(0), aTarget - aFuzz); int hops = 0; ogg_int64_t previousGuess = -1; int backsteps = 0; const int maxBackStep = 10; NS_ASSERTION(static_cast(PAGE_STEP) * pow(2.0, maxBackStep) < PR_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. while (PR_TRUE) { ogg_int64_t duration = 0; double target = 0; ogg_int64_t interval = 0; ogg_int64_t guess = 0; ogg_page page; int skippedBytes = 0; ogg_int64_t pageOffset = 0; ogg_int64_t pageLength = 0; ogg_int64_t granuleTime = -1; PRBool mustBackoff = PR_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 (PR_TRUE) { // Discard any previously buffered packets/pages. if (NS_FAILED(ResetDecode())) { 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(PR_LOG_DEBUG, ("Interval narrowed, terminating bisection.")); break; } // Guess bisection point. duration = endTime - startTime; target = (double)(seekTarget - startTime) / (double)duration; guess = startOffset + startLength + static_cast((double)interval * target); guess = NS_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(PR_LOG_DEBUG, ("Backing off %d bytes, backsteps=%d", static_cast(PAGE_STEP * pow(2.0, backsteps)), backsteps)); guess -= PAGE_STEP * static_cast(pow(2.0, backsteps)); backsteps = NS_MIN(backsteps + 1, maxBackStep); // We reset mustBackoff. If we still need to backoff further, it will // be set to PR_TRUE again. mustBackoff = PR_FALSE; } else { backsteps = 0; } guess = NS_MAX(guess, startOffset + startLength); SEEK_LOG(PR_LOG_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)); NS_ASSERTION(guess >= startOffset + startLength, "Guess must be after range start"); NS_ASSERTION(guess < endOffset, "Guess must be before range end"); NS_ASSERTION(guess != previousGuess, "Guess should be differnt 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 res = PageSync(stream, &mOggState, PR_FALSE, guess, endOffset, &page, skippedBytes); NS_ENSURE_TRUE(res != PAGE_SYNC_ERROR, NS_ERROR_FAILURE); // We've located a page of length |ret| at |guess + skippedBytes|. // Remember where the page is located. pageOffset = guess + skippedBytes; pageLength = page.header_len + page.body_len; mPageOffset = pageOffset + pageLength; if (res == 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 = PR_TRUE; SEEK_LOG(PR_LOG_DEBUG, ("Hit the end of range, backing off")); continue; } // 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. PRUint32 serial = ogg_page_serialno(&page); nsOggCodecState* codecState = nsnull; mCodecStates.Get(serial, &codecState); if (codecState && codecState->mActive) { int ret = ogg_stream_pagein(&codecState->mState, &page); NS_ENSURE_TRUE(ret == 0, NS_ERROR_FAILURE); } ogg_int64_t granulepos = ogg_page_granulepos(&page); if (HasAudio() && granulepos != -1 && serial == mVorbisState->mSerial && audioTime == -1) { audioTime = mVorbisState->Time(granulepos); } if (HasVideo() && granulepos != -1 && serial == mTheoraState->mSerial && videoTime == -1) { videoTime = mTheoraState->StartTime(granulepos); } if (mPageOffset == endOffset) { // Hit end of readable data. break; } if (ReadOggPage(&page) == -1) { break; } } while ((mVorbisState && audioTime == -1) || (mTheoraState && videoTime == -1)); NS_ASSERTION(mPageOffset <= endOffset, "Page read cursor should be inside range"); if ((HasAudio() && audioTime == -1) || (HasVideo() && videoTime == -1)) { // We don't have timestamps for all active tracks... if (pageOffset == startOffset + startLength && mPageOffset == 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 = PR_TRUE; continue; } // We've found appropriate time stamps here. Proceed to bisect // the search space. granuleTime = NS_MAX(audioTime, videoTime); NS_ASSERTION(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(PR_LOG_DEBUG, ("Terminating seek at offset=%lld", startOffset)); NS_ASSERTION(startTime < aTarget, "Start time must always be less than target"); res = stream->Seek(nsISeekableStream::NS_SEEK_SET, startOffset); NS_ENSURE_SUCCESS(res,res); mPageOffset = startOffset; if (NS_FAILED(ResetDecode())) { return NS_ERROR_FAILURE; } break; } SEEK_LOG(PR_LOG_DEBUG, ("Time at offset %lld is %lldms", guess, granuleTime)); if (granuleTime < seekTarget && granuleTime > seekLowerBound) { // We're within the fuzzy region in which we want to terminate the search. res = stream->Seek(nsISeekableStream::NS_SEEK_SET, pageOffset); NS_ENSURE_SUCCESS(res,res); mPageOffset = pageOffset; if (NS_FAILED(ResetDecode())) { return NS_ERROR_FAILURE; } SEEK_LOG(PR_LOG_DEBUG, ("Terminating seek at offset=%lld", mPageOffset)); break; } if (granuleTime >= seekTarget) { // We've landed after the seek target. NS_ASSERTION(pageOffset < endOffset, "offset_end must decrease"); endOffset = pageOffset; endTime = granuleTime; } else if (granuleTime < seekTarget) { // Landed before seek target. NS_ASSERTION(pageOffset > startOffset, "offset_start must increase"); startOffset = pageOffset; startLength = pageLength; startTime = granuleTime; } NS_ASSERTION(startTime < seekTarget, "Must be before seek target"); NS_ASSERTION(endTime >= seekTarget, "End must be after seek target"); } SEEK_LOG(PR_LOG_DEBUG, ("Seek complete in %d bisections.", hops)); return NS_OK; } nsresult nsOggReader::GetBuffered(nsTimeRanges* aBuffered, PRInt64 aStartTime) { // 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 and GetBuffered isn't called before metadata is // read. if (!mInfo.mHasVideo && !mInfo.mHasAudio) { // No need to search through the file if there are no audio or video tracks return NS_OK; } nsMediaStream* stream = mDecoder->GetCurrentStream(); nsTArray ranges; nsresult res = stream->GetCachedRanges(ranges); NS_ENSURE_SUCCESS(res, res); // Traverse across the buffered byte ranges, determining the time ranges // they contain. nsMediaStream::GetNextCachedData(offset) returns -1 when // offset is after the end of the media stream, 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. ogg_sync_state state; ogg_sync_init(&state); for (PRUint32 index = 0; index < ranges.Length(); index++) { // Ensure the offsets are after the header pages. PRInt64 startOffset = NS_MAX(ranges[index].mStart, mDataOffset); PRInt64 endOffset = NS_MAX(ranges[index].mEnd, mDataOffset); // Because the granulepos time is actually the end time of the page, // we special-case (startOffset == mDataOffset) 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. PRInt64 startTime = (startOffset == mDataOffset) ? aStartTime : -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. ogg_sync_reset(&state); while (startTime == -1) { ogg_page page; PRInt32 discard; PageSyncResult res = PageSync(stream, &state, PR_TRUE, startOffset, endOffset, &page, discard); if (res == PAGE_SYNC_ERROR) { // If we don't clear the sync state before exit we'll leak. ogg_sync_clear(&state); return NS_ERROR_FAILURE; } else if (res == 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; } PRInt64 granulepos = ogg_page_granulepos(&page); if (granulepos == -1) { // Page doesn't have an end time, advance to the next page // until we find one. startOffset += page.header_len + page.body_len; continue; } PRUint32 serial = ogg_page_serialno(&page); if (mVorbisState && serial == mVorbisSerial) { startTime = nsVorbisState::Time(&mVorbisInfo, granulepos) - aStartTime; NS_ASSERTION(startTime > 0, "Must have positive start time"); } else if (mTheoraState && serial == mTheoraSerial) { startTime = nsTheoraState::Time(&mTheoraInfo, granulepos) - aStartTime; NS_ASSERTION(startTime > 0, "Must have positive start time"); } else if (IsKnownStream(serial)) { // Stream is not the theora or vorbis stream we're playing, // but is one that we have header data for. startOffset += page.header_len + page.body_len; continue; } else { // Page is for a stream we don't know about (possibly a chained // ogg), return an error. ogg_sync_clear(&state); return PAGE_SYNC_ERROR; } } if (startTime != -1) { // We were able to find a start time for that range, see if we can // find an end time. PRInt64 endTime = FindEndTime(startOffset, endOffset, PR_TRUE, &state); if (endTime != -1) { aBuffered->Add(startTime / static_cast(USECS_PER_S), (endTime - aStartTime) / static_cast(USECS_PER_S)); } } } // If we don't clear the sync state before exit we'll leak. ogg_sync_clear(&state); return NS_OK; } PRBool nsOggReader::IsKnownStream(PRUint32 aSerial) { for (PRUint32 i = 0; i < mKnownStreams.Length(); i++) { PRUint32 serial = mKnownStreams[i]; if (serial == aSerial) { return PR_TRUE; } } return PR_FALSE; }