michael@0: /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #ifndef MOZILLA_TRACKUNIONSTREAM_H_ michael@0: #define MOZILLA_TRACKUNIONSTREAM_H_ michael@0: michael@0: #include "MediaStreamGraph.h" michael@0: #include michael@0: michael@0: namespace mozilla { michael@0: michael@0: #ifdef PR_LOGGING michael@0: #define STREAM_LOG(type, msg) PR_LOG(gMediaStreamGraphLog, type, msg) michael@0: #else michael@0: #define STREAM_LOG(type, msg) michael@0: #endif michael@0: michael@0: /** michael@0: * See MediaStreamGraph::CreateTrackUnionStream. michael@0: * This file is only included by MediaStreamGraph.cpp so it's OK to put the michael@0: * entire implementation in this header file. michael@0: */ michael@0: class TrackUnionStream : public ProcessedMediaStream { michael@0: public: michael@0: TrackUnionStream(DOMMediaStream* aWrapper) : michael@0: ProcessedMediaStream(aWrapper), michael@0: mFilterCallback(nullptr), michael@0: mMaxTrackID(0) {} michael@0: michael@0: virtual void RemoveInput(MediaInputPort* aPort) MOZ_OVERRIDE michael@0: { michael@0: for (int32_t i = mTrackMap.Length() - 1; i >= 0; --i) { michael@0: if (mTrackMap[i].mInputPort == aPort) { michael@0: EndTrack(i); michael@0: mTrackMap.RemoveElementAt(i); michael@0: } michael@0: } michael@0: ProcessedMediaStream::RemoveInput(aPort); michael@0: } michael@0: virtual void ProcessInput(GraphTime aFrom, GraphTime aTo, uint32_t aFlags) MOZ_OVERRIDE michael@0: { michael@0: if (IsFinishedOnGraphThread()) { michael@0: return; michael@0: } michael@0: nsAutoTArray mappedTracksFinished; michael@0: nsAutoTArray mappedTracksWithMatchingInputTracks; michael@0: for (uint32_t i = 0; i < mTrackMap.Length(); ++i) { michael@0: mappedTracksFinished.AppendElement(true); michael@0: mappedTracksWithMatchingInputTracks.AppendElement(false); michael@0: } michael@0: bool allFinished = true; michael@0: bool allHaveCurrentData = true; michael@0: for (uint32_t i = 0; i < mInputs.Length(); ++i) { michael@0: MediaStream* stream = mInputs[i]->GetSource(); michael@0: if (!stream->IsFinishedOnGraphThread()) { michael@0: // XXX we really should check whether 'stream' has finished within time aTo, michael@0: // not just that it's finishing when all its queued data eventually runs michael@0: // out. michael@0: allFinished = false; michael@0: } michael@0: if (!stream->HasCurrentData()) { michael@0: allHaveCurrentData = false; michael@0: } michael@0: for (StreamBuffer::TrackIter tracks(stream->GetStreamBuffer()); michael@0: !tracks.IsEnded(); tracks.Next()) { michael@0: bool found = false; michael@0: for (uint32_t j = 0; j < mTrackMap.Length(); ++j) { michael@0: TrackMapEntry* map = &mTrackMap[j]; michael@0: if (map->mInputPort == mInputs[i] && map->mInputTrackID == tracks->GetID()) { michael@0: bool trackFinished; michael@0: StreamBuffer::Track* outputTrack = mBuffer.FindTrack(map->mOutputTrackID); michael@0: if (!outputTrack || outputTrack->IsEnded()) { michael@0: trackFinished = true; michael@0: } else { michael@0: CopyTrackData(tracks.get(), j, aFrom, aTo, &trackFinished); michael@0: } michael@0: mappedTracksFinished[j] = trackFinished; michael@0: mappedTracksWithMatchingInputTracks[j] = true; michael@0: found = true; michael@0: break; michael@0: } michael@0: } michael@0: if (!found && (!mFilterCallback || mFilterCallback(tracks.get()))) { michael@0: bool trackFinished = false; michael@0: uint32_t mapIndex = AddTrack(mInputs[i], tracks.get(), aFrom); michael@0: CopyTrackData(tracks.get(), mapIndex, aFrom, aTo, &trackFinished); michael@0: mappedTracksFinished.AppendElement(trackFinished); michael@0: mappedTracksWithMatchingInputTracks.AppendElement(true); michael@0: } michael@0: } michael@0: } michael@0: for (int32_t i = mTrackMap.Length() - 1; i >= 0; --i) { michael@0: if (mappedTracksFinished[i]) { michael@0: EndTrack(i); michael@0: } else { michael@0: allFinished = false; michael@0: } michael@0: if (!mappedTracksWithMatchingInputTracks[i]) { michael@0: mTrackMap.RemoveElementAt(i); michael@0: } michael@0: } michael@0: if (allFinished && mAutofinish && (aFlags & ALLOW_FINISH)) { michael@0: // All streams have finished and won't add any more tracks, and michael@0: // all our tracks have actually finished and been removed from our map, michael@0: // so we're finished now. michael@0: FinishOnGraphThread(); michael@0: } else { michael@0: mBuffer.AdvanceKnownTracksTime(GraphTimeToStreamTime(aTo)); michael@0: } michael@0: if (allHaveCurrentData) { michael@0: // We can make progress if we're not blocked michael@0: mHasCurrentData = true; michael@0: } michael@0: } michael@0: michael@0: // Consumers may specify a filtering callback to apply to every input track. michael@0: // Returns true to allow the track to act as an input; false to reject it entirely. michael@0: typedef bool (*TrackIDFilterCallback)(StreamBuffer::Track*); michael@0: void SetTrackIDFilter(TrackIDFilterCallback aCallback) { michael@0: mFilterCallback = aCallback; michael@0: } michael@0: michael@0: // Forward SetTrackEnabled(output_track_id, enabled) to the Source MediaStream, michael@0: // translating the output track ID into the correct ID in the source. michael@0: virtual void ForwardTrackEnabled(TrackID aOutputID, bool aEnabled) MOZ_OVERRIDE { michael@0: for (int32_t i = mTrackMap.Length() - 1; i >= 0; --i) { michael@0: if (mTrackMap[i].mOutputTrackID == aOutputID) { michael@0: mTrackMap[i].mInputPort->GetSource()-> michael@0: SetTrackEnabled(mTrackMap[i].mInputTrackID, aEnabled); michael@0: } michael@0: } michael@0: } michael@0: michael@0: protected: michael@0: TrackIDFilterCallback mFilterCallback; michael@0: michael@0: // Only non-ended tracks are allowed to persist in this map. michael@0: struct TrackMapEntry { michael@0: // mEndOfConsumedInputTicks is the end of the input ticks that we've consumed. michael@0: // 0 if we haven't consumed any yet. michael@0: TrackTicks mEndOfConsumedInputTicks; michael@0: // mEndOfLastInputIntervalInInputStream is the timestamp for the end of the michael@0: // previous interval which was unblocked for both the input and output michael@0: // stream, in the input stream's timeline, or -1 if there wasn't one. michael@0: StreamTime mEndOfLastInputIntervalInInputStream; michael@0: // mEndOfLastInputIntervalInOutputStream is the timestamp for the end of the michael@0: // previous interval which was unblocked for both the input and output michael@0: // stream, in the output stream's timeline, or -1 if there wasn't one. michael@0: StreamTime mEndOfLastInputIntervalInOutputStream; michael@0: MediaInputPort* mInputPort; michael@0: // We keep track IDs instead of track pointers because michael@0: // tracks can be removed without us being notified (e.g. michael@0: // when a finished track is forgotten.) When we need a Track*, michael@0: // we call StreamBuffer::FindTrack, which will return null if michael@0: // the track has been deleted. michael@0: TrackID mInputTrackID; michael@0: TrackID mOutputTrackID; michael@0: nsAutoPtr mSegment; michael@0: }; michael@0: michael@0: uint32_t AddTrack(MediaInputPort* aPort, StreamBuffer::Track* aTrack, michael@0: GraphTime aFrom) michael@0: { michael@0: // Use the ID of the source track if we can, otherwise allocate a new michael@0: // unique ID michael@0: TrackID id = std::max(mMaxTrackID + 1, aTrack->GetID()); michael@0: mMaxTrackID = id; michael@0: michael@0: TrackRate rate = aTrack->GetRate(); michael@0: // Round up the track start time so the track, if anything, starts a michael@0: // little later than the true time. This means we'll have enough michael@0: // samples in our input stream to go just beyond the destination time. michael@0: TrackTicks outputStart = TimeToTicksRoundUp(rate, GraphTimeToStreamTime(aFrom)); michael@0: michael@0: nsAutoPtr segment; michael@0: segment = aTrack->GetSegment()->CreateEmptyClone(); michael@0: for (uint32_t j = 0; j < mListeners.Length(); ++j) { michael@0: MediaStreamListener* l = mListeners[j]; michael@0: l->NotifyQueuedTrackChanges(Graph(), id, rate, outputStart, michael@0: MediaStreamListener::TRACK_EVENT_CREATED, michael@0: *segment); michael@0: } michael@0: segment->AppendNullData(outputStart); michael@0: StreamBuffer::Track* track = michael@0: &mBuffer.AddTrack(id, rate, outputStart, segment.forget()); michael@0: STREAM_LOG(PR_LOG_DEBUG, ("TrackUnionStream %p adding track %d for input stream %p track %d, start ticks %lld", michael@0: this, id, aPort->GetSource(), aTrack->GetID(), michael@0: (long long)outputStart)); michael@0: michael@0: TrackMapEntry* map = mTrackMap.AppendElement(); michael@0: map->mEndOfConsumedInputTicks = 0; michael@0: map->mEndOfLastInputIntervalInInputStream = -1; michael@0: map->mEndOfLastInputIntervalInOutputStream = -1; michael@0: map->mInputPort = aPort; michael@0: map->mInputTrackID = aTrack->GetID(); michael@0: map->mOutputTrackID = track->GetID(); michael@0: map->mSegment = aTrack->GetSegment()->CreateEmptyClone(); michael@0: return mTrackMap.Length() - 1; michael@0: } michael@0: void EndTrack(uint32_t aIndex) michael@0: { michael@0: StreamBuffer::Track* outputTrack = mBuffer.FindTrack(mTrackMap[aIndex].mOutputTrackID); michael@0: if (!outputTrack || outputTrack->IsEnded()) michael@0: return; michael@0: for (uint32_t j = 0; j < mListeners.Length(); ++j) { michael@0: MediaStreamListener* l = mListeners[j]; michael@0: TrackTicks offset = outputTrack->GetSegment()->GetDuration(); michael@0: nsAutoPtr segment; michael@0: segment = outputTrack->GetSegment()->CreateEmptyClone(); michael@0: l->NotifyQueuedTrackChanges(Graph(), outputTrack->GetID(), michael@0: outputTrack->GetRate(), offset, michael@0: MediaStreamListener::TRACK_EVENT_ENDED, michael@0: *segment); michael@0: } michael@0: outputTrack->SetEnded(); michael@0: } michael@0: void CopyTrackData(StreamBuffer::Track* aInputTrack, michael@0: uint32_t aMapIndex, GraphTime aFrom, GraphTime aTo, michael@0: bool* aOutputTrackFinished) michael@0: { michael@0: TrackMapEntry* map = &mTrackMap[aMapIndex]; michael@0: StreamBuffer::Track* outputTrack = mBuffer.FindTrack(map->mOutputTrackID); michael@0: MOZ_ASSERT(outputTrack && !outputTrack->IsEnded(), "Can't copy to ended track"); michael@0: michael@0: TrackRate rate = outputTrack->GetRate(); michael@0: MediaSegment* segment = map->mSegment; michael@0: MediaStream* source = map->mInputPort->GetSource(); michael@0: michael@0: GraphTime next; michael@0: *aOutputTrackFinished = false; michael@0: for (GraphTime t = aFrom; t < aTo; t = next) { michael@0: MediaInputPort::InputInterval interval = map->mInputPort->GetNextInputInterval(t); michael@0: interval.mEnd = std::min(interval.mEnd, aTo); michael@0: StreamTime inputEnd = source->GraphTimeToStreamTime(interval.mEnd); michael@0: TrackTicks inputTrackEndPoint = TRACK_TICKS_MAX; michael@0: michael@0: if (aInputTrack->IsEnded() && michael@0: aInputTrack->GetEndTimeRoundDown() <= inputEnd) { michael@0: inputTrackEndPoint = aInputTrack->GetEnd(); michael@0: *aOutputTrackFinished = true; michael@0: } michael@0: michael@0: if (interval.mStart >= interval.mEnd) michael@0: break; michael@0: next = interval.mEnd; michael@0: michael@0: // Ticks >= startTicks and < endTicks are in the interval michael@0: StreamTime outputEnd = GraphTimeToStreamTime(interval.mEnd); michael@0: TrackTicks startTicks = outputTrack->GetEnd(); michael@0: StreamTime outputStart = GraphTimeToStreamTime(interval.mStart); michael@0: NS_WARN_IF_FALSE(startTicks == TimeToTicksRoundUp(rate, outputStart), michael@0: "Samples missing"); michael@0: TrackTicks endTicks = TimeToTicksRoundUp(rate, outputEnd); michael@0: TrackTicks ticks = endTicks - startTicks; michael@0: StreamTime inputStart = source->GraphTimeToStreamTime(interval.mStart); michael@0: michael@0: if (interval.mInputIsBlocked) { michael@0: // Maybe the input track ended? michael@0: segment->AppendNullData(ticks); michael@0: STREAM_LOG(PR_LOG_DEBUG+1, ("TrackUnionStream %p appending %lld ticks of null data to track %d", michael@0: this, (long long)ticks, outputTrack->GetID())); michael@0: } else { michael@0: // Figuring out which samples to use from the input stream is tricky michael@0: // because its start time and our start time may differ by a fraction michael@0: // of a tick. Assuming the input track hasn't ended, we have to ensure michael@0: // that 'ticks' samples are gathered, even though a tick boundary may michael@0: // occur between outputStart and outputEnd but not between inputStart michael@0: // and inputEnd. michael@0: // These are the properties we need to ensure: michael@0: // 1) Exactly 'ticks' ticks of output are produced, i.e. michael@0: // inputEndTicks - inputStartTicks = ticks. michael@0: // 2) inputEndTicks <= aInputTrack->GetSegment()->GetDuration(). michael@0: // 3) In any sequence of intervals where neither stream is blocked, michael@0: // the content of the input track we use is a contiguous sequence of michael@0: // ticks with no gaps or overlaps. michael@0: if (map->mEndOfLastInputIntervalInInputStream != inputStart || michael@0: map->mEndOfLastInputIntervalInOutputStream != outputStart) { michael@0: // Start of a new series of intervals where neither stream is blocked. michael@0: map->mEndOfConsumedInputTicks = TimeToTicksRoundDown(rate, inputStart) - 1; michael@0: } michael@0: TrackTicks inputStartTicks = map->mEndOfConsumedInputTicks; michael@0: TrackTicks inputEndTicks = inputStartTicks + ticks; michael@0: map->mEndOfConsumedInputTicks = inputEndTicks; michael@0: map->mEndOfLastInputIntervalInInputStream = inputEnd; michael@0: map->mEndOfLastInputIntervalInOutputStream = outputEnd; michael@0: // Now we prove that the above properties hold: michael@0: // Property #1: trivial by construction. michael@0: // Property #3: trivial by construction. Between every two michael@0: // intervals where both streams are not blocked, the above if condition michael@0: // is false and mEndOfConsumedInputTicks advances exactly to match michael@0: // the ticks that were consumed. michael@0: // Property #2: michael@0: // Let originalOutputStart be the value of outputStart and originalInputStart michael@0: // be the value of inputStart when the body of the "if" block was last michael@0: // executed. michael@0: // Let allTicks be the sum of the values of 'ticks' computed since then. michael@0: // The interval [originalInputStart/rate, inputEnd/rate) is the michael@0: // same length as the interval [originalOutputStart/rate, outputEnd/rate), michael@0: // so the latter interval can have at most one more integer in it. Thus michael@0: // TimeToTicksRoundUp(rate, outputEnd) - TimeToTicksRoundUp(rate, originalOutputStart) michael@0: // <= TimeToTicksRoundDown(rate, inputEnd) - TimeToTicksRoundDown(rate, originalInputStart) + 1 michael@0: // Then michael@0: // inputEndTicks = TimeToTicksRoundDown(rate, originalInputStart) - 1 + allTicks michael@0: // = TimeToTicksRoundDown(rate, originalInputStart) - 1 + TimeToTicksRoundUp(rate, outputEnd) - TimeToTicksRoundUp(rate, originalOutputStart) michael@0: // <= TimeToTicksRoundDown(rate, originalInputStart) - 1 + TimeToTicksRoundDown(rate, inputEnd) - TimeToTicksRoundDown(rate, originalInputStart) + 1 michael@0: // = TimeToTicksRoundDown(rate, inputEnd) michael@0: // <= inputEnd/rate michael@0: // (now using the fact that inputEnd <= track->GetEndTimeRoundDown() for a non-ended track) michael@0: // <= TicksToTimeRoundDown(rate, aInputTrack->GetSegment()->GetDuration())/rate michael@0: // <= rate*aInputTrack->GetSegment()->GetDuration()/rate michael@0: // = aInputTrack->GetSegment()->GetDuration() michael@0: // as required. michael@0: michael@0: if (inputStartTicks < 0) { michael@0: // Data before the start of the track is just null. michael@0: // We have to add a small amount of delay to ensure that there is michael@0: // always a sample available if we see an interval that contains a michael@0: // tick boundary on the output stream's timeline but does not contain michael@0: // a tick boundary on the input stream's timeline. 1 tick delay is michael@0: // necessary and sufficient. michael@0: segment->AppendNullData(-inputStartTicks); michael@0: inputStartTicks = 0; michael@0: } michael@0: if (inputEndTicks > inputStartTicks) { michael@0: segment->AppendSlice(*aInputTrack->GetSegment(), michael@0: std::min(inputTrackEndPoint, inputStartTicks), michael@0: std::min(inputTrackEndPoint, inputEndTicks)); michael@0: } michael@0: STREAM_LOG(PR_LOG_DEBUG+1, ("TrackUnionStream %p appending %lld ticks of input data to track %d", michael@0: this, (long long)(std::min(inputTrackEndPoint, inputEndTicks) - std::min(inputTrackEndPoint, inputStartTicks)), michael@0: outputTrack->GetID())); michael@0: } michael@0: ApplyTrackDisabling(outputTrack->GetID(), segment); michael@0: for (uint32_t j = 0; j < mListeners.Length(); ++j) { michael@0: MediaStreamListener* l = mListeners[j]; michael@0: l->NotifyQueuedTrackChanges(Graph(), outputTrack->GetID(), michael@0: outputTrack->GetRate(), startTicks, 0, michael@0: *segment); michael@0: } michael@0: outputTrack->GetSegment()->AppendFrom(segment); michael@0: } michael@0: } michael@0: michael@0: nsTArray mTrackMap; michael@0: TrackID mMaxTrackID; michael@0: }; michael@0: michael@0: } michael@0: michael@0: #endif /* MOZILLA_MEDIASTREAMGRAPH_H_ */