// Calls fileStream.Position and fileStream.Write only. // // Usually trackSamples has 2 elements: a video track and an audio track. // // Uses trackEntries and trackSamples only as a read-only argument, doesn't modify their contents. // // Starts with the initial cue points specified in cuePoints, and appends subsequent cue points in place. private static void WriteClustersAndCues(FileStream fileStream, ulong segmentOffset, int videoTrackIndex, IList<bool> isAmsCodecs, IMediaDataSource mediaDataSource, MuxStateWriter muxStateWriter, IList<CuePoint> cuePoints, ref ulong minStartTime, ulong timePosition, out ulong seekHeadOffsetMS, out ulong cuesOffsetMS) { int trackCount = mediaDataSource.GetTrackCount(); if (isAmsCodecs.Count != trackCount) { throw new Exception("ASSERT: isAmsCodecs vs mediaDataSource length mismatch."); } if (trackCount > 13) { // 13 is because a..m and n..z in MuxStateWriter checkpointing. throw new Exception("Too many tracks to mux."); } // For each track, contains the data bytes of a media sample ungot (i.e. pushed back) after reading. // Initializes items to null (good). MediaDataBlock[] ungetBlocks = new MediaDataBlock[trackCount]; ulong minStartTime0 = minStartTime; if (timePosition == ulong.MaxValue) { timePosition = 0; ulong maxStartTime = ulong.MaxValue; for (int i = 0; i < trackCount; ++i) { if ((ungetBlocks[i] = mediaDataSource.PeekBlock(i)) != null) { if (maxStartTime == ulong.MaxValue || maxStartTime < ungetBlocks[i].StartTime) { maxStartTime = ungetBlocks[i].StartTime; } mediaDataSource.ConsumeBlock(i); // Since it was moved to ungetBlocks[i]. } } for (int i = 0; i < trackCount; ++i) { MediaDataBlock block = mediaDataSource.PeekBlock(i); while (block != null && block.StartTime <= maxStartTime) { ungetBlocks[i] = block; // Takes ownership. mediaDataSource.ConsumeBlock(i); } // We'll start each track (in ungetMediaSample[i]) from the furthest sample within maxStartTime. } int trackIndex2; if ((trackIndex2 = GetNextTrackIndex(mediaDataSource, ungetBlocks)) < 0) { throw new Exception("ASSERT: Empty media file, no samples."); } minStartTime = minStartTime0 = ungetBlocks[trackIndex2] != null ? ungetBlocks[trackIndex2].StartTime : mediaDataSource.PeekBlock(trackIndex2).StartTime; muxStateWriter.WriteUlong('A', minStartTime0); } List<ArraySegment<byte>> output = new List<ArraySegment<byte>>(); ulong[] lastOutputStartTimes = new ulong[trackCount]; // Items initialized to zero. int trackIndex; // timePosition is the beginning StartTime of the last output block written by fileStream.Write. while ((trackIndex = GetNextTrackIndex(mediaDataSource, ungetBlocks)) >= 0) { ulong timeCode; // Will be set below. bool isKeyFrame; // Will be set below. MediaDataBlock block0; // Will be set below. MediaDataBlock block1 = null; // May be set below. int mediaDataBlockTotalSize; // Will be set below. { if ((block0 = ungetBlocks[trackIndex]) == null && (block0 = mediaDataSource.PeekBlock(trackIndex)) == null) { throw new Exception("ASSERT: Reading from a track already at EOF."); } // Some kind of time delta for this sample. timeCode = block0.StartTime - timePosition - minStartTime0; if (block0.StartTime < timePosition + minStartTime0) { throw new Exception("Bad start times: block0.StartTime=" + block0.StartTime + " timePosition=" + timePosition + " minStartTime=" + minStartTime0); } isKeyFrame = block0.IsKeyFrame; mediaDataBlockTotalSize = block0.Bytes.Count; if (ungetBlocks[trackIndex] != null) { ungetBlocks[trackIndex] = null; } else { mediaDataSource.ConsumeBlock(trackIndex); } } if (timeCode > 327670000uL) { throw new Exception("timeCode too large: " + timeCode); // Maybe that's not fatal? } if (isAmsCodecs[trackIndex]) { // Copy one more MediaSample if available. // TODO: Test this. block1 = ungetBlocks[trackIndex]; if (block1 != null) { mediaDataBlockTotalSize += block1.Bytes.Count; ungetBlocks[trackIndex] = null; } else if ((block1 = mediaDataSource.PeekBlock(trackIndex)) != null) { mediaDataBlockTotalSize += block1.Bytes.Count; mediaDataSource.ConsumeBlock(trackIndex); } } // TODO: How can be timeCode so large at this point? if ((output.Count != 0 && trackIndex == videoTrackIndex && isKeyFrame) || timeCode > 327670000uL) { ulong outputOffset = (ulong)fileStream.Position - segmentOffset; cuePoints.Add(new CuePoint(timePosition / 10000uL, (ulong)(videoTrackIndex + 1), outputOffset)); muxStateWriter.WriteUlong('C', timePosition); muxStateWriter.WriteUlong('D', outputOffset); int totalSize = 0; for (int i = 0; i < output.Count; ++i) { totalSize += output[i].Count; } // We do a single copy of the media stream data bytes here. That copy is inevitable, because it's // faster to save to file that way. byte[] bytes = Utils.CombineByteArraysAndArraySegments( new byte[][]{GetDataSizeBytes((ulong)ID.Cluster), GetDataSizeBytes((ulong)totalSize)}, output); output.Clear(); // The average bytes.Length is 286834 bytes here, that's large enough (>8 kB), and it doesn't warrant a // a buffered output stream for speedup. fileStream.Write(bytes, 0, bytes.Length); fileStream.Flush(); for (int i = 0; i < trackCount; ++i) { muxStateWriter.WriteUlong((char)('a' + i), lastOutputStartTimes[i]); } muxStateWriter.WriteUlong('P', (ulong)bytes.Length); muxStateWriter.Flush(); } if (output.Count == 0) { timePosition += timeCode; timeCode = 0uL; output.Add(new ArraySegment<byte>( GetEEBytes(ID.Timecode, GetVintBytes(timePosition / 10000uL)))); } output.Add(new ArraySegment<byte>(GetSimpleBlockBytes( (ulong)(trackIndex + 1), (short)(timeCode / 10000uL), isKeyFrame, isAmsCodecs[trackIndex], mediaDataBlockTotalSize))); output.Add(block0.Bytes); if (block1 != null) output.Add(block1.Bytes); lastOutputStartTimes[trackIndex] = block1 != null ? block1.StartTime : block0.StartTime; } // Write remaining samples (from output to fileStream), and write cuePoints. { ulong outputOffset = (ulong)fileStream.Position - segmentOffset; cuePoints.Add(new CuePoint(timePosition / 10000uL, (ulong)(videoTrackIndex + 1), outputOffset)); muxStateWriter.WriteUlong('C', timePosition); muxStateWriter.WriteUlong('D', outputOffset); if (output.Count == 0) { throw new Exception("ASSERT: Expecting non-empty output at end of mixing."); } int totalSize = 0; for (int i = 0; i < output.Count; ++i) { totalSize += output[i].Count; } byte[] bytes = Utils.CombineByteArraysAndArraySegments( new byte[][]{GetDataSizeBytes((ulong)ID.Cluster), GetDataSizeBytes((ulong)totalSize)}, output); output.Clear(); // Save memory. cuesOffsetMS = outputOffset + (ulong)bytes.Length; byte[] bytes2 = GetCueBytes(cuePoints); // cues are about 1024 bytes per 2 minutes. seekHeadOffsetMS = cuesOffsetMS + (ulong)bytes2.Length; SeekBlock[] seekBlocks = new SeekBlock[cuePoints.Count]; for (int i = 0; i < cuePoints.Count; ++i) { seekBlocks[i] = new SeekBlock(ID.Cluster, cuePoints[i].CueClusterPosition); } byte[] bytes3 = GetSeekBytes(seekBlocks, -1); bytes = Utils.CombineBytes(bytes, bytes2, bytes3); fileStream.Write(bytes, 0, bytes.Length); } }
// Returns trackIndex with the smallest StartTime, or -1. private static int GetNextTrackIndex(IMediaDataSource mediaDataSource, MediaDataBlock[] ungetBlocks) { int trackCount = ungetBlocks.Length; // == mediaDataSource.GetTrackCount(). ulong minUnconsumedStartTime = 0; // No real need to initialize it here. int trackIndex = -1; for (int i = 0; i < trackCount; ++i) { MediaDataBlock block = ungetBlocks[i]; if (block == null) block = mediaDataSource.PeekBlock(i); if (block != null && (trackIndex == -1 || minUnconsumedStartTime > block.StartTime)) { trackIndex = i; minUnconsumedStartTime = block.StartTime; } } return trackIndex; }