Пример #1
0
 // 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();
     }
       }
 }
Пример #2
0
        // 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);
              }
        }
Пример #3
0
 // Takes ownership of trackChunkStartTimes (and will append to its items).
 public StateChunkStartTimeReceiver(MuxStateWriter muxStateWriter, IList<ulong>[] trackChunkStartTimes)
 {
     this.MuxStateWriter = muxStateWriter;
     this.TrackChunkStartTimes = trackChunkStartTimes;
     this.TrackChunkWrittenCounts = new int[trackChunkStartTimes.Length];  // Initializes items to 0.
     for (int trackIndex = 0; trackIndex < trackChunkStartTimes.Length; ++trackIndex) {
       IList<ulong> chunkStartTimes = trackChunkStartTimes[trackIndex];
       if (chunkStartTimes == null) {
     trackChunkStartTimes[trackIndex] = chunkStartTimes = new List<ulong>();
       } else {
     int chunkCount = chunkStartTimes.Count;
     for (int chunkIndex = 1; chunkIndex < chunkCount; ++chunkIndex) {
       if (chunkStartTimes[chunkIndex - 1] >= chunkStartTimes[chunkIndex]) {
     throw new Exception(string.Concat(new object[] {
         "Chunk StartTimes not increasing: track=", trackIndex,
         " chuunk=", chunkIndex }));
       }
     }
       }
       this.TrackChunkWrittenCounts[trackIndex] = trackChunkStartTimes[trackIndex].Count;
     }
 }
Пример #4
0
        // Exactly one of manifestUri and manifestPath must be set.
        public static void DownloadAndMux(Uri manifestUri, string manifestPath, string mkvPath, bool isDeterministic, TimeSpan stopAfter,
            SetupStop setupStop, DisplayDuration displayDuration)
        {
            string manifestParentPath = null;  // A null indicates a remote manifest file.
              ManifestInfo manifestInfo;
              if (manifestPath != null) {
            manifestParentPath = Path.GetDirectoryName(manifestPath);
            Console.WriteLine("Parsing local manifest file: " + manifestPath);
            using (FileStream manifestStream = new FileStream(manifestPath, FileMode.Open)) {
              manifestInfo = ManifestInfo.ParseManifest(manifestStream, /*manifestUri:*/new Uri(LOCAL_URL_PREFIX));
            }
                        } else {
            Console.WriteLine("Downloading and parsing manifest: " + manifestUri);
            WebClient webClient = new WebClient();
            using (Stream manifestStream = webClient.OpenRead(manifestUri)) {
              manifestInfo = ManifestInfo.ParseManifest(manifestStream, manifestUri);
            }
              }
              Console.Write(manifestInfo.GetDescription());

              IList<Track> tracks = new List<Track>();
              foreach (StreamInfo streamInfo in manifestInfo.SelectedStreams) {
            foreach (TrackInfo trackInfo in streamInfo.SelectedTracks) {
              tracks.Add(new Track(trackInfo));
            }
              }
              IList<TrackEntry> trackEntries = new List<TrackEntry>();
              IList<IList<MediaSample>> trackSamples = new List<IList<MediaSample>>();
              for (int i = 0; i < tracks.Count; ++i) {
            trackEntries.Add(tracks[i].TrackInfo.TrackEntry);
            trackEntries[i].TrackNumber = (ulong)(i + 1);
            trackSamples.Add(new List<MediaSample>());
              }
              for (int i = 0; i < tracks.Count; i++) {
            // TODO: Add a facility to start live streams from a later chunk (it was chunkIndex=10 previously).
            // Our design allows for an empty ChunkList, in case live streams are growing.
            tracks[i].NextStartTime = tracks[i].TrackInfo.Stream.ChunkList.Count == 0 ? 0 :
                                  tracks[i].TrackInfo.Stream.ChunkList[0].StartTime;
              }
              // TODO: Test for live streams (see the StackOverflow question).
              Console.WriteLine("Also muxing selected tracks to MKV: " + mkvPath);
              try {
            if (Directory.GetParent(mkvPath) != null &&
            !Directory.GetParent(mkvPath).Exists)
              Directory.GetParent(mkvPath).Create();
              } catch (IOException) {
            // TODO: Add nicer error reporting, without a stack trace.
            throw new Exception("Cannot not create the directory of .mkv: " + mkvPath);
              }
              ulong maxTrackEndTimeHint = manifestInfo.Duration;
              for (int i = 0; i < tracks.Count; ++i) {
            IList<ChunkInfo> chunkInfos = tracks[i].TrackInfo.Stream.ChunkList;
            int j = chunkInfos.Count - 1;
            if (j >= 0) {  // Our design allows for an empty ChunkList.
              ulong trackDuration = chunkInfos[j].StartTime + chunkInfos[j].Duration;
              if (maxTrackEndTimeHint < trackDuration) maxTrackEndTimeHint = trackDuration;
            }
              }
              // The .muxstate file is approximately 1/5441.43 of the size of the .mkv.
              // The .muxstate file is around 28.088 bytes per second. TODO: Update this after n.
              // Sometimes totalDuration of video is 1156420602, audio is 1156818141 (larger), so we just take the maximum.
              string muxStatePath = Path.ChangeExtension(mkvPath, "muxstate");
              string muxStateOldPath = muxStatePath + ".old";
              byte[] oldMuxState = null;
              if (File.Exists(muxStatePath)) {  // False for directories.
            using (FileStream fileStream = new FileStream(muxStatePath, FileMode.Open)) {
              oldMuxState = ReadFileStream(fileStream);
            }
            if (oldMuxState.Length > 0) {
              // File.Move fails with IOException if the destination already exists.
              // C# and .NET SUXX: There is no atomic overwrite-move.
              try {
            File.Move(muxStatePath, muxStateOldPath);
              } catch (IOException) {
            File.Replace(muxStatePath, muxStateOldPath, null, true);
              }
            }
              }
              DownloadingMediaDataSource source = new DownloadingMediaDataSource(
              tracks, manifestParentPath, manifestInfo.TimeScale,
                            manifestInfo.IsLive, (ulong)stopAfter.Ticks, manifestInfo.TotalTicks, displayDuration);
              setupStop(manifestInfo.IsLive, source);
              MuxStateWriter muxStateWriter = new MuxStateWriter(new FileStream(muxStatePath, FileMode.Create));
              try {
            MkvUtils.WriteMkv(mkvPath, trackEntries, source, maxTrackEndTimeHint, manifestInfo.TimeScale, isDeterministic,
                          oldMuxState, muxStateWriter);
              } finally {
            muxStateWriter.Close();
              }
              File.Delete(muxStatePath);
              if (File.Exists(muxStateOldPath)) {
            File.Delete(muxStateOldPath);
              }
        }