// 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); } }
// This function may modify trackSamples in a destructive way, to save memory. public static void WriteMkv(string mkvPath, IList<TrackEntry> trackEntries, IMediaDataSource mediaDataSource, ulong maxTrackEndTimeHint, ulong timeScale, bool isDeterministic, byte[] oldMuxState, MuxStateWriter muxStateWriter) { if (trackEntries.Count != mediaDataSource.GetTrackCount()) { throw new Exception("ASSERT: trackEntries vs mediaDataSource length mismatch."); } bool doParseOldMuxState = oldMuxState != null && oldMuxState.Length > 0; FileMode fileMode = doParseOldMuxState ? FileMode.OpenOrCreate : FileMode.Create; using (FileStream fileStream = new FileStream(mkvPath, fileMode)) { ulong oldSize = doParseOldMuxState ? (ulong)fileStream.Length : 0uL; int videoTrackIndex = GetVideoTrackIndex(trackEntries, 0); bool isComplete = false; bool isContinuable = false; ulong lastOutOfs = 0; ulong segmentOffset = 0; // Will be overwritten below. IList<CuePoint> cuePoints = null; ulong minStartTime = 0; ulong timePosition = ulong.MaxValue; byte[] prefix = null; // Well be overwritten below. if (doParseOldMuxState && oldSize > 0) { Console.WriteLine("Trying to use the old mux state to continue downloading."); prefix = new byte[4096]; int prefixSize = fileStream.Read(prefix, 0, prefix.Length); ParsedMuxState parsedMuxState = ParseMuxState( oldMuxState, oldSize, prefix, prefixSize, videoTrackIndex, trackEntries.Count); if (parsedMuxState.isComplete) { Console.WriteLine("The .mkv file is already fully downloaded."); isComplete = true; // TODO: Don't even temporarily modify the .muxstate file. muxStateWriter.WriteRaw(oldMuxState, 0, oldMuxState.Length); } else if (parsedMuxState.isContinuable) { Console.WriteLine("Continuing the .mkv file download."); lastOutOfs = parsedMuxState.lastOutOfs; segmentOffset = parsedMuxState.vS; cuePoints = parsedMuxState.cuePoints; minStartTime = parsedMuxState.vA; timePosition = parsedMuxState.lastC; // We may save memory after this by trucating prefix to durationOffset + 4 -- but we don't care. muxStateWriter.WriteRaw(oldMuxState, 0, parsedMuxState.endOffset); mediaDataSource.StartChunks(new StateChunkStartTimeReceiver( muxStateWriter, parsedMuxState.trackChunkStartTimes)); for (int i = 0; i < trackEntries.Count; ++i) { // Skip downloading most of the chunk files already in the .mkv (up to lastOutOfs). mediaDataSource.ConsumeBlocksUntil(i, parsedMuxState.trackLastStartTimes[i]); } isContinuable = true; } else { Console.WriteLine("Could not use old mux state: " + parsedMuxState); } } if (!isComplete) { fileStream.SetLength((long)lastOutOfs); fileStream.Seek((long)lastOutOfs, 0); if (!isContinuable) { // Not continuing from previous state, writing an .mkv from scratch. // EBML: http://matroska.org/technical/specs/rfc/index.html // http://matroska.org/technical/specs/index.html prefix = GetEbmlHeaderBytes(); segmentOffset = (ulong)prefix.Length + 12; muxStateWriter.WriteUlong('X', MUX_STATE_VERSION); // Unique ID and version number. muxStateWriter.WriteUlong('S', segmentOffset); // About 52. prefix = Utils.CombineBytes(prefix, GetSegmentBytes( /*duration:*/maxTrackEndTimeHint, INITIAL_MEDIA_END_OFFSET_MS, INITIAL_SEEK_HEAD_OFFSET_MS, INITIAL_CUES_OFFSET_MS, timeScale, trackEntries, isDeterministic)); fileStream.Write(prefix, 0, prefix.Length); // Write the MKV header. fileStream.Flush(); muxStateWriter.WriteBytes('H', prefix); // About 405 bytes long. muxStateWriter.WriteUlong('V', (ulong)videoTrackIndex); cuePoints = new List<CuePoint>(); mediaDataSource.StartChunks(new StateChunkStartTimeReceiver( muxStateWriter, new IList<ulong>[trackEntries.Count])); } ulong seekHeadOffsetMS; // Will be set by WriteClustersAndCues below. ulong cuesOffsetMS; // Will be set by WriteClustersAndCues below. WriteClustersAndCues( fileStream, segmentOffset, videoTrackIndex, GetIsAmsCodecs(trackEntries), mediaDataSource, muxStateWriter, cuePoints, ref minStartTime, timePosition, out seekHeadOffsetMS, out cuesOffsetMS); fileStream.Flush(); // Update the MKV header with the file size. ulong mediaEndOffset = (ulong)fileStream.Position; muxStateWriter.WriteUlong('M', mediaEndOffset); // Usually this seek position is 45. ulong maxTrackEndTime = 0; // TODO: mkvmerge calculates this differently (<0.5s -- rounding?) for (int i = 0; i < mediaDataSource.GetTrackCount(); ++i) { ulong trackEndTime = mediaDataSource.GetTrackEndTime(i); if (maxTrackEndTime < trackEndTime) maxTrackEndTime = trackEndTime; } // Update the ID.Segment size and ID.Duration with their final values. int seekOffset = (int)segmentOffset - 7; // We update the final duration and some offsets in the .mkv header so mplayer (and possibly other // media players) will be able to seek in the file without additional tricks. More specifically: // // play-before play-after seek-before seek-after // mplayer yes yes no yes // mplayer -idx yes yes yes yes // mplayer2 yes yes yes yes // mplayer2 -idx yes yes yes yes // VLC 1.0.x no no no no // VLC 1.1.x no no no no // VLC 2.0.x ? yes ? yes // SMPlayer 0.6.9 ? yes ? yes // // Legend: // // * mplayer: MPlayer SVN-r1.0~rc3+svn20090426-4.4.3 on Ubuntu Lucid // * mplayer2: MPlayer2 2.0 from http://ftp.mplayer2.org/pub/release/ , mtime 2011-03-26 // * -idx; The -idx command-line flag of mplayer and mplayer2. // * play: playing the video sequentially from beginning to end // * seek: jumping back and forth within the video upon user keypress (e.g. the <Up> key), // including jumping to regions of the .mkv which haven't been downloaded when playback started // * before: before running UpdatePrefix below, i.e. while the .mkv is being downloaded // * after: after running UpdatePrefix below // // VLC 1.0.x and VLC 1.1.x problems: audio is fine, but the video is jumping back and forth fraction of // a second. int updateOffset = UpdatePrefix( prefix, prefix.Length, segmentOffset, mediaEndOffset - segmentOffset, /*seekHeadOffsetMS:*/seekHeadOffsetMS, /*cuesOffsetMS:*/cuesOffsetMS, /*duration:*/maxTrackEndTime - minStartTime, timeScale); fileStream.Seek(seekOffset, 0); fileStream.Write(prefix, seekOffset, updateOffset - seekOffset); fileStream.Flush(); muxStateWriter.WriteUlong('Z', 1); muxStateWriter.Flush(); } } }
// 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; }