From 2f305e1efac50a4b688d8c1f9d19f387b722a7cd Mon Sep 17 00:00:00 2001 From: Weibing Zhan Date: Fri, 4 Aug 2023 13:21:08 -0700 Subject: [PATCH] Add Parser/Deserializer for fmp4 boxes Description Add fmp4 boxes parser/deserializer for common boxes which are used by Live, LiveToVOD, smooth or cmaf files. moov, mdat, moof and its children boxes etc. mfra as smooth specific box is not included. tfdt box as cmaf specific box is included. new boxes can be added later when needed. All the unknown boxes are treated as basic Box with its own four-letter type. The code is ported over from our source tree with matching namespace and compatible with new c# compiler. Internal tests have passed for different types of boxes. --- fmp4/Box.cs | 973 +++++++++++++++++++++++++++ fmp4/BoxExtensions.cs | 69 ++ fmp4/Fmp4Fragment.cs | 600 +++++++++++++++++ fmp4/Fmp4FragmentSample.cs | 319 +++++++++ fmp4/Fmp4FragmentSampleExtensions.cs | 192 ++++++ fmp4/FullBox.cs | 297 ++++++++ fmp4/MP4BoxFactory.cs | 123 ++++ fmp4/MP4BoxType.cs | 105 +++ fmp4/MP4DeserializeException.cs | 103 +++ fmp4/MP4Reader.cs | 132 ++++ fmp4/MP4Writer.cs | 136 ++++ fmp4/VariableLengthField.cs | 325 +++++++++ fmp4/hdlrBox.cs | 373 ++++++++++ fmp4/mdatBox.cs | 376 +++++++++++ fmp4/mdhdBox.cs | 498 ++++++++++++++ fmp4/mdiaBox.cs | 192 ++++++ fmp4/mfhdBox.cs | 195 ++++++ fmp4/moofBox.cs | 133 ++++ fmp4/moovBox.cs | 72 ++ fmp4/mvexBox.cs | 49 ++ fmp4/mvhdBox.cs | 359 ++++++++++ fmp4/sdtpBox.cs | 232 +++++++ fmp4/sdtpEntry.cs | 244 +++++++ fmp4/tfdtBox.cs | 210 ++++++ fmp4/tfhdBox.cs | 471 +++++++++++++ fmp4/tkhdBox.cs | 343 ++++++++++ fmp4/trafBox.cs | 142 ++++ fmp4/trakBox.cs | 129 ++++ fmp4/trexBox.cs | 252 +++++++ fmp4/trunBox.cs | 539 +++++++++++++++ fmp4/trunEntry.cs | 268 ++++++++ 31 files changed, 8451 insertions(+) create mode 100644 fmp4/Box.cs create mode 100644 fmp4/BoxExtensions.cs create mode 100644 fmp4/Fmp4Fragment.cs create mode 100644 fmp4/Fmp4FragmentSample.cs create mode 100644 fmp4/Fmp4FragmentSampleExtensions.cs create mode 100644 fmp4/FullBox.cs create mode 100644 fmp4/MP4BoxFactory.cs create mode 100644 fmp4/MP4BoxType.cs create mode 100644 fmp4/MP4DeserializeException.cs create mode 100644 fmp4/MP4Reader.cs create mode 100644 fmp4/MP4Writer.cs create mode 100644 fmp4/VariableLengthField.cs create mode 100644 fmp4/hdlrBox.cs create mode 100644 fmp4/mdatBox.cs create mode 100644 fmp4/mdhdBox.cs create mode 100644 fmp4/mdiaBox.cs create mode 100644 fmp4/mfhdBox.cs create mode 100644 fmp4/moofBox.cs create mode 100644 fmp4/moovBox.cs create mode 100644 fmp4/mvexBox.cs create mode 100644 fmp4/mvhdBox.cs create mode 100644 fmp4/sdtpBox.cs create mode 100644 fmp4/sdtpEntry.cs create mode 100644 fmp4/tfdtBox.cs create mode 100644 fmp4/tfhdBox.cs create mode 100644 fmp4/tkhdBox.cs create mode 100644 fmp4/trafBox.cs create mode 100644 fmp4/trakBox.cs create mode 100644 fmp4/trexBox.cs create mode 100644 fmp4/trunBox.cs create mode 100644 fmp4/trunEntry.cs diff --git a/fmp4/Box.cs b/fmp4/Box.cs new file mode 100644 index 0000000..5be00b8 --- /dev/null +++ b/fmp4/Box.cs @@ -0,0 +1,973 @@ + + +using System.Globalization; +using System.Text; +using System.Diagnostics; +using System.Net; +using System.Collections.ObjectModel; + +namespace AMSMigrate.Fmp4 +{ + /// + /// Implements the Box object defined in ISO 14496-12. + /// + /// Everything within an MP4 object is a Box. This implementation of Box includes size and type, + /// as per the spec, but also includes what we call the "Body" or content of the box, which is to say + /// the bytes referred to by the size field. + /// + /// Thus, just by itself, this Box implementation can either represent an unknown Box (including its contents), + /// and be serialized as such (its contents will be written as well). Or, if the box is recognized, then + /// a class which derives from Box is expected to fully consume the Body during its deserialization. + /// + /// The design of this Box supports round-trip deserialization-serialization whenever possible. There + /// are certain known exceptions to this, such as when Box.Size is expressed as "0" on disk. Thus, if the + /// Box.Size is expressed using extended (64-bit) sizing, but the value of Size is small and therefore would + /// fit into 32-bits, this class does not automatically downshift to compact (32-bit) size representation on + /// serialization. The serialization would continue to use 64-bit size fields. If, however, the user modifies + /// the box (Box.Dirty becomes true), then size would be written in compact (32-bit) form. Even this can be + /// overridden to always force 64-bit sizes, but by default, this class does support round-trip. + /// + /// The general pattern for deserialization of a known box type is therefore: + /// 1) Deserialize Box from a Stream. At this stage it is treated as an "unknown" box. The act of deserializing + /// makes the box type available, and saves the Body (contents) for future use. + /// 2) Use the boxtype to instantiate an object which understands it. This is done via a class derived from Box which + /// contains a constructor taking a Box as its argument. The Box (including its contents) are copied to the base + /// instance via copy constructor. + /// 3) The derived class then fully consumes the Body.Body stream as it deserializes. + /// 4) The chain of derived classes are expected to mark themselves as clean (ie. Dirty = false), but NOT + /// to touch the dirty state of another class, including base classes. In this way, Box.Size can be dirty + /// because it was set to 0 on-disk, but the derived classes can themselves be clean. The overall value + /// of Dirty in this case would be false. This was necessary to decouple the dependency chain, ie. not have a + /// base class call the derived class's overridden Dirty function during construction, before the + /// derived class is itself constructed. + /// + /// The general pattern for serialization is: + /// 1) Call Box.WriteTo. + /// 2) This calls to the virtual function, Box.WriteToInternal, which all derived classes should override. + /// 3) Serialization therefore always proceeds from derived class down through base classes. + /// + public class Box : IEquatable + { + ~Box() + { + _children.CollectionChanged -= Children_CollectionChanged; + } + + /// + /// Constructor for compact type. + /// Only used by derived classes. + /// + /// The compact type. + protected Box(UInt32 boxtype) : + this(boxtype, null) + { + } + + /// + /// Constructor for extended type ('uuid' box). + /// Only used by derived classes. + /// + /// The extended type. + protected Box(Guid extendedType) + :this(MP4BoxType.uuid, extendedType) + { + } + + /// + /// Constructor to intialize with a type and an optional sub type. + /// Private constructor to keep all initialization at one place. + /// + /// the type of the box + /// the extended type if any + private Box(UInt32 boxtype, Guid? extendedType) + { + UInt32 size = 4 + 4; // Size (32) + Type (32) + Type = boxtype; + _dirty = true; + + if (extendedType != null) + { + ExtendedType = extendedType; + size += 16; //ExtendedType (128) + } + + int[] validBitDepths = { 32, 64 }; + Size = new VariableLengthField(validBitDepths, size); + + _children.CollectionChanged += Children_CollectionChanged; + } + + /// + /// Listen to changes to the children. + /// + /// the sender of the event + /// event parameter + private void Children_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) + { + //mark the size as dirty. + Size.Dirty = true; + } + + /// + /// Copy constructor. Only used by derived classes while deserializing. + /// + /// + protected Box(Box other) + { + _dirty = false; //By default not dirty for deserialization. + + Type = other.Type; + ExtendedType = other.ExtendedType; + Size = other.Size; + + Body = other.Body; + BodyInitialOffset = other.BodyInitialOffset; + BodyPreBytes = other.BodyPreBytes; + + if (Body != null) + { + //mark begin of deserialization. + _deserializing = true; + + ReadBody(); + ConsumeBody(); + + if (Size.Value == 0) + { + SetDirty(); + } + + _deserializing = false; + } + else + { + //Copy constructor. we current do not have member copy so mark as dirty since we dont round trip. + SetDirty(); + } + + _children.CollectionChanged += Children_CollectionChanged; //register after the children are parsed. + } + + /// + /// This is overriden by the derived classes to read the actual body of the box. + /// + /// true if the whole body is consumed else false. + protected virtual void ReadBody() + { + } + + /// + /// Deserializing constructor. Currently, deserialization requires a stream which + /// can provide Length and Seek (eg. CanSeek == true). If it becomes important to + /// read from non-seeking streams (ie. streaming parser), this class can be modified, + /// derived, etc. The vast majority of boxes are small enough that writing a streaming + /// parser would be a waste of time. The main beneficiary would be big boxes like 'mdat'. + /// + /// Binary reader for use during deserialization. + /// The file offset which corresponds with + /// reader.BaseStream.Position == 0. This is used to get the correct file offset in error messages. + public Box(MP4Reader reader, long initialOffset = 0) + { + long startPosition = reader.BaseStream.Position; + bool dirty = false; // We just deserialized, so by default we're not dirty - unless size == 0 + long bytesRead; + + try + { + int[] validBitDepths = { 32, 64 }; + Size = new VariableLengthField(validBitDepths, reader.ReadUInt32()); + + Type = reader.ReadUInt32(); + + // Size field may be compact (32-bit) or extended (64-bit), with special case for 0. + if (1 == Size.Value) + { + // This signals that actual size is in the largesize field + Size.Value = reader.ReadUInt64(); + Size.BitDepth = 64; // Must set this AFTER setting Value, to ensure round-trip + } + else if (0 == Size.Value) + { + // This signals that this is last box in file, Size extends to EOF + Size.Value = (UInt64)(reader.BaseStream.Length - startPosition); + Size.BitDepth = 32; // Must set this AFTER setting Value + + // This makes us dirty because value on disk is 0, but we set to actual size on disk + // This means, by the way, we will never be able to serialize out a size of 0, either. + dirty = true; + } + else + { + Size.BitDepth = 32; // Must set this AFTER setting Value, to ensure round-trip + } + + // Check if size is valid - it must clear the number of bytes we have already read + // We will already have read past the desired size, but I don't care - for instance, + // if I set Size == 2, just reading the 4 bytes then puts us over. So this cannot be avoided. + bytesRead = reader.BaseStream.Position - startPosition; + if ((UInt64)bytesRead > Size.Value) + { + // Reported error offset will point to start of box + throw new MP4DeserializeException(TypeDescription, startPosition, initialOffset, + String.Format(CultureInfo.InvariantCulture, "Stated size of {0} is too small, we have already read {1} bytes!", + Size.Value, bytesRead)); + } + + if (MP4BoxType.uuid == Type) + { + const long sizeOfGuid = 16; + + // Before reading the GUID, check if we have enough size + if ((UInt64)(bytesRead + sizeOfGuid) > Size.Value) + { + // Reported error offset will point to start of box + throw new MP4DeserializeException(TypeDescription, startPosition, initialOffset, + String.Format(CultureInfo.InvariantCulture, "Stated size of {0} is too small, must be at least {1} to clear the extended user type", + Size.Value, bytesRead + sizeOfGuid)); + } + + ExtendedType = reader.ReadGuid(); + } + else + { + Debug.Assert(null == ExtendedType); + } + } + catch (EndOfStreamException ex) + { + String errMsg; + + errMsg = String.Format(CultureInfo.InvariantCulture, "Unexpected end-of-stream, box.Size was {0}, stream length was {1}", + Size!.Value, reader.BaseStream.Length - startPosition); + + // Reported error offset will point to start of box + throw new MP4DeserializeException(TypeDescription, startPosition, initialOffset, errMsg, ex); + } + + // We are not dirty, we just deserialized - unless Box.Size was 0 on disk. + Size.Dirty = dirty; + + SaveBody(reader, initialOffset, startPosition); + + } + + /// + /// Save the content of the body of the box as a stream. + /// + /// The reader from which the box is being parsed + /// The file offset that corresponds to reader.BaseStreamPosition == 0 + /// The position with in the stream where the box starts. + private void SaveBody(MP4Reader reader, long initialOffset, long startPosition) + { + // Save the remainder of the stream for further processing by derived classes + long bytesRead = reader.BaseStream.Position - startPosition; + + if (Size.Value > Int32.MaxValue) + { + // This size is so large that when casting to integer below, it will go negative. + // Any attempt to allocate this amount of memory (actually, even less than this), + // will fail, so bail out now. + + // Reported error offset will point to start of Box.Body + throw new MP4DeserializeException(TypeDescription, startPosition + bytesRead, initialOffset, + String.Format(CultureInfo.InvariantCulture, "Error saving Box.Body. Size of {0} is too large for this implementation of Box to handle.", + Size.Value)); + } + + // Before actually allocating the array and reading from the stream, let's see if the + // stream length is sufficient to satisfy our request. + int bytesToCopy = (int)(Size.Value - (UInt64)bytesRead); + if (bytesToCopy > reader.BaseStream.Length - reader.BaseStream.Position) + { + long shortfall = bytesToCopy - (reader.BaseStream.Length - reader.BaseStream.Position); + + // Reported error offset will point to start of Box.Body + throw new MP4DeserializeException(TypeDescription, startPosition + bytesRead, initialOffset, + String.Format(CultureInfo.InvariantCulture, "Error saving Box.Body. About to read {0} bytes, stream length is {1} bytes short", + bytesToCopy, shortfall)); + } + + Byte[] bytes = new Byte[bytesToCopy]; + int bytesCopied = reader.BaseStream.Read(bytes, 0, bytes.Length); + + // Even though the stream length was checked to be sufficent, the stream could still end up underdelivering + // Check for this here. + if (bytesCopied < bytesToCopy) + { + // Reported error offset will point to start of Box.Body + throw new MP4DeserializeException(TypeDescription, startPosition + bytesRead, initialOffset, + String.Format(CultureInfo.InvariantCulture, "Error saving Box.Body. Trying to read {0} bytes, hit end-of-stream {1} bytes short", + bytesToCopy, bytesToCopy - bytesCopied)); + } + + MemoryStream stream = new MemoryStream(bytes); + Body = new MP4Reader(stream); + BodyInitialOffset = initialOffset + startPosition + bytesRead; + BodyPreBytes = bytesRead; + } + + /// + /// This helper function allows non-Box entities to peek at the size and compact type of the next box + /// in the stream. This is used to enhance security, because once we commit to deserializing the full + /// box, we are compelled to allocate memory to store the body. + /// + /// Returns the compact size of this box. + /// Returns the compact type of this box. + /// The MP4Reader to read from. + /// The file offset which corresponds with + /// reader.BaseStream.Position == 0. This is used to get the correct file offset in error messages. + public static void PeekCompactType(out UInt32 size, + out UInt32 type, + BinaryReader reader, + long initialOffset = 0) + { + long startPosition = reader.BaseStream.Position; + bool haveSize = false; + + // Initialize outputs + size = 0; + type = 0; + + try + { + size = reader.ReadUInt32(); + haveSize = true; + type = reader.ReadUInt32(); + + reader.BaseStream.Position = startPosition; // Rewind stream + } + catch (EndOfStreamException ex) + { + String errMsg; + if (false == haveSize) + { + errMsg = String.Format(CultureInfo.InvariantCulture, "Unexpected end-of-stream, box.Size could not be deserialized"); + } + else + { + errMsg = String.Format(CultureInfo.InvariantCulture, "Unexpected end-of-stream, box.Size was {0}, stream length was {1}", + size, reader.BaseStream.Length - startPosition); + } + + // Reported error offset will point to start of box + throw new MP4DeserializeException("(unknown box)", startPosition, initialOffset, errMsg, ex); + } + } + + /// + /// True if this box matches the on-disk representation, either because we deserialized and + /// no further changes were made, or because we just saved to disk and no further changes were made. + /// The derived class is expected to override this property to incorporate its own Dirty state + /// in the result. + /// + public virtual bool Dirty + { + get + { + if (_dirty) + { + return true; + } + + if (Size.Dirty) + return true; + + //If any of the child boxes are dirty then we are dirty too. + foreach (Box child in _children) + { + if (child.Dirty) + return true; + } + + // If we reached this stage, we are not dirty + return false; + } + set + { + if (_deserializing) + { + throw new InvalidOperationException("Cannot set Dirty while deserializing. Call SetDirty instead"); + } + + _dirty = value; + + Size.Dirty = value; + + foreach (Box child in _children) + { + child.Dirty = value; + } + } + } + + /// + /// Marks the box contents to be dirty so that size needs to be recomputed. + /// + protected void SetDirty() + { + _dirty = true; + } + + /// + /// This variable tracks miscellaneous sources of dirtiness from direct members of this class, + /// + private bool _dirty; + + /// + /// This variable indicate we are currently deserializing the box. + /// Derived classes should only call SetDirty() to mark the box dirty instead of .Dirty=true. + /// + private bool _deserializing; + + + /// + /// Returns the child boxes of this box. + /// + public IList Children => _children; + + + /// + /// An observable collection of child boxes for this box. + /// + private ObservableCollection _children = new ObservableCollection(); + + /// + /// The size of the box, as currently expressed on disk. The latter is especially important to note. + /// This field DOES NOT return the current size of the box, especially after modifications have been made. + /// + /// If Box.Dirty is false, then the caller can be assured that Box.Size represents the declared box size + /// in the bitstream as encountered during deserialization, or as written during serialization. + /// + /// If Box.Dirty is true, then changes have been made (for instance, entries added to 'trun', etc). The + /// Box.Size does not automatically update as a result of this. If it is necessary to know what the size + /// currently is, then one should call Box.ComputeSize(). + /// + /// The other thing to mention is the special case there this is the last box in the file, and the box size + /// was expressed as "0" on disk (meaning that the box extends to end of file). This box object does not + /// have a representation for this state, so it simply substitutes the actual number of bytes from here to + /// end of file. In such a case, Box.Dirty will be set to true to signal the fact that Size, although correct, + /// does not represent the on-disk value. + /// + /// By default, the bit-depth either matches the on-disk bit depth, or else is automatically adjusted when + /// Box.Dirty is true. The BitDepth can also be set and locked, if desired. See VariableLengthField for more details. + /// + public VariableLengthField Size { get; private set; } + + /// + /// The "type" field as per ISO 14496-12. + /// + public UInt32 Type { get; private set; } + + /// + /// The extended_type field as per ISO 14496-12. This is set to null if this box uses a compact type. + /// + public Guid? ExtendedType { get; private set; } + + /// + /// Returns a human-readable string describing the "type" field. If this is a compact + /// type, then the ASCII representation is returned within single-quotes, eg. 'moof'. + /// If user extended type, then this may return something like, + /// uuid:d899e70d-cc78-475a-aaa6-0a890d62895c. + /// + public string TypeDescription + { + get + { + string result; + + if (MP4BoxType.uuid == Type) + { + // It's OK if ExtendedType is null, no exceptions appear to be thrown + result = String.Format(CultureInfo.InvariantCulture, "uuid:{0}", ExtendedType); + } + else + { + Debug.Assert(null == ExtendedType); + result = CompactTypeToString(Type); + } + + return result; + } + } + + /// + /// Helper function to convert a compact type to a string, eg. 'trun'. + /// + /// The compact type value to convert to string. + /// A string form of the compact type, eg. 'trun'. + public static string CompactTypeToString(UInt32 type) + { + Encoding wireEncoding = Encoding.UTF8; + Byte[] typeBytes = BitConverter.GetBytes(IPAddress.NetworkToHostOrder((Int32)type)); + return String.Format(CultureInfo.InvariantCulture, "'{0}'", wireEncoding.GetString(typeBytes)); + } + + /// + /// The contents of this Box. + /// This is used by derived classes to deserialize themselves. + /// + protected MP4Reader? Body { get; private set; } + + /// + /// The file offset which corresponds with Body.BaseStream.Position == 0. This is used + /// by derived classes to get the correct file offset in error messages. + /// + protected long BodyInitialOffset { get; private set; } + + /// + /// The number of bytes from start of this box to start of Body (ie. the bytes used to + /// convey size/type/extended size/extended type). This is used to by derived classes + /// to report an offset to the start of this box when an error is encountered during + /// deserialization. + /// + protected long BodyPreBytes { get; private set; } + + /// + /// This is called at the end of deserialization, when box has fully consumed the Body. + /// + private void ConsumeBody() + { + if (Body?.BaseStream.Position < Body?.BaseStream.Length) + { + // Reported error offset will point just past end of currently processed Box + throw new MP4DeserializeException(TypeDescription, Body.BaseStream.Position, BodyInitialOffset, + String.Format(CultureInfo.InvariantCulture, "Failure to fully read (or skip) Box.Body during deserialization, {0} bytes unhandled", + Body.BaseStream.Length - Body.BaseStream.Position)); + } + + Body?.Dispose(); + Body = null; + BodyInitialOffset = 0; + BodyPreBytes = 0; + } + + /// + /// This computes the size of this box, particularly after modifications have been made. + /// If Box.Dirty is false, one would expect ComputeSize() to exactly equal the on-disk + /// representation (Box.Size). If Box.Dirty is true, then ComputeSize() will return the + /// current size of the box, while Box.Size will be stale (ie. incorrect). + /// + /// It is expected that derived classes will override this method and aggregate their + /// own sizes plus their base class sizes. + /// + /// The current size of this box, if it were to be written to disk now. + public virtual UInt64 ComputeSize() + { + UInt64 size = ComputeLocalSize(); + + foreach (Box child in _children) + { + size += child.ComputeSize(); + } + + return size; + } + + /// + /// Calculates the current size of just this class (base classes excluded). This is called by + /// ComputeSize(). + /// + /// It is expected that derived classes will "new" this method (ie. hide the inherited method). + /// + /// The current size of just the fields from this box. + protected UInt64 ComputeLocalSize() + { + UInt64 thisSize = 0; + + thisSize += 4; // 32-bit size field + thisSize += 4; // 32-bit type field + + if (64 == Size.BitDepth) + { + thisSize += 8; // 64-bit size field + } + + if (MP4BoxType.uuid == Type) + { + thisSize += 16; // Extended type GUID + } + + if (null != Body) + { + thisSize += (UInt64)Body.BaseStream.Length; + } + + return thisSize; + } + + /// + /// Computes the size of the box portion (size, type) of a derived box type, + /// for the purpose of computing offsets within the derived box type. For instance, + /// by calling this method, we can calculate the offset within the mdatBox where + /// mdatBox.SampleData starts. + /// + /// The size of the box portion of the current derived box type. + public UInt64 ComputeBaseSizeBox() + { + return ComputeLocalSize(); + } + + /// + /// This function serializes the box to disk. In doing so, Box.Size must be updated. At the end of + /// serialization, Box.Dirty will be false. This is a base class function which engages the derived + /// class by calling the virtual function, WriteToInternal, which is not exposed to the public. + /// By doing it this way, things which are always done during serialization (updating Size if Dirty, + /// setting Dirty to false at the end) can be done once here, and not require cut-and-paste into + /// all derived classes. + /// + /// The MP4Writer to write to. + public void WriteTo(MP4Writer writer) + { + long startPosition = writer.BaseStream.Position; + + // The top WriteTo call is responsible for setting Box.Size, but only if Dirty + if (Dirty) + { + UInt64 newSize = ComputeSize(); + Size.Value = newSize; + } + + // This call actually performs the serialization + WriteToInternal(writer); + + WriteChildren(writer); + + // Now handle post-serialization tasks + Dirty = false; // We just wrote to disk, our contents match exactly + + // Confirm that we wrote what we said we would write + Debug.Assert(writer.BaseStream.Position - startPosition == (long)Size.Value || + Stream.Null == writer.BaseStream); + } + + /// + /// Serialize the child boxes to the writer. + /// + /// The writer to write the boxes. + protected virtual void WriteChildren(MP4Writer writer) + { + //Write the child boxes. + foreach (Box child in _children) + { + child.WriteTo(writer); + } + + } + + /// + /// This function does the actual work of serializing this class to disk. It is expected that derived + /// classes will override this function. By the time this function is called, Box.Size has already been + /// updated to reflect the current size of the box as reported by ComputeSize(). Also, this function + /// does not need to set dirty state. It should simply serialize. + /// + /// The MP4Writer to write to. + protected virtual void WriteToInternal(MP4Writer writer) + { + UInt64 thisSize = ComputeLocalSize(); + long startPosition = writer.BaseStream.Position; + + bool largesize; + if (64 == Size.BitDepth) + { + largesize = true; + } + else + { + largesize = false; + } + + // Write the 32-bit size field + if (largesize) + { + writer.WriteUInt32(1); + } + else + { + writer.WriteUInt32((UInt32)Size.Value); + } + + // Write the type field + writer.WriteUInt32(Type); + + // Write the 64-bit size field + if (largesize) + { + writer.WriteUInt64(Size.Value); + } + + // Write the extended type field + if (MP4BoxType.uuid == Type && ExtendedType != null) + { + writer.Write(ExtendedType.Value); + } + + // If the body was not consumed, this means this box was unknown. Write out the Body, unchanged + if (null != Body) + { + Body.BaseStream.Position = 0; // Rewind the stream + Body.BaseStream.CopyTo(writer.BaseStream); + } + + // Confirm that we wrote what we said we would write + Debug.Assert(writer.BaseStream.Position - startPosition == (long)thisSize || + Stream.Null == writer.BaseStream); + } + + + /// + /// Parse the body to read the child boxes and add it to the _children collection. + /// + protected void ReadChildren() + { + long fileOffset = BodyInitialOffset; + + if (Body != null) + { + // This is a container box, so use the MP4BoxFactory to deserialize the contained boxes. + while (Body.BaseStream.Position < Body.BaseStream.Length) + { + long streamOffset = Body.BaseStream.Position; + + Box subbox = MP4BoxFactory.ParseSingleBox(Body, fileOffset); + + //Make sure we advanced after reading a box. Avoid infinite loop and break if not. + if (Body.BaseStream.Position <= streamOffset) + { + break; + } + + //check we did not cross the stream while parsing a box. + Debug.Assert(Body.BaseStream.Position <= Body.BaseStream.Length); + + _children.Add(subbox); + + fileOffset += Body.BaseStream.Position; + } + } + } + + /// + /// Helper function to get exactly one box of the requested type, + /// and to throw if exactly one cannot be found. + /// + /// The type of the requested child box. + /// A box of the requested type. + public TChildBoxType? GetExactlyOneChildBox() where TChildBoxType : Box + { + IEnumerable boxes = _children.Where(b => b is TChildBoxType); + int numBoxes = boxes.Count(); + if (1 != numBoxes) + { + // Perform a reverse dictionary lookup to find the name of this child box + string childBoxName; + IEnumerable> compactBoxTypes = MP4BoxType.CompactType.Where(entry => entry.Value == typeof(TChildBoxType)); + if (1 != compactBoxTypes.Count()) + { + // We don't want to throw an exception, as we are already in the middle of throwing one, so just use class name + childBoxName = typeof(TChildBoxType).Name; + } + else + { + childBoxName = Box.CompactTypeToString(compactBoxTypes.Single().Key); + } + + throw new InvalidOperationException( + String.Format(CultureInfo.InvariantCulture, "{0} expected exactly 1 {1}, instead found {2}!", + GetType().Name, childBoxName, numBoxes)); + } + + return boxes.Single() as TChildBoxType; + } + + /// + /// Remove those children selected by the given child selector from Children property (list). + /// + /// A function which will select the children to remove. + public void RemoveChildren(Func shouldRemove) + { + if (null == shouldRemove) + { + throw new ArgumentNullException(nameof(shouldRemove)); + } + + int numChildren = _children.Count; + for (int i = numChildren - 1; i >= 0; i--) + { + if (shouldRemove(_children[i])) + { + _children.RemoveAt(i); + } + } + } + + #region Equality Methods + + //===================================================================== + // Equality Methods + // + // In order to implement IEquatable the way in which MSDN recommends, + // it is unfortunately necessary to cut-and-paste this group of functions + // into each derived class and to do a search-and-replace on the types + // to match the derived class type. Generic classes do not work because + // of the special rules around operator overloading and such. + // + // In addition to this cut-and-paste, search-and-replace, the derived + // class should also override the Equals(base) method so that programs + // which attempt to test equality using pointers to base classes will + // be seeing results from the fully derived equality implementations. + // + // In other words, Box box = new DerivedBox(), box.Equals(box2). If + // Equals(base) is not overriden, then the base implementation of + // Equals(base) is used, which will not compare derived fields. + //===================================================================== + + /// + /// Object.Equals override. + /// + /// The object to test equality against. + /// True if the this and the given object are equal. + public override bool Equals(Object? other) + { + return this.Equals(other as Box); + } + + /// + /// Implements IEquatable(Box). This function is virtual and it is expected that + /// derived classes will override it, so that programs which attempt to test equality + /// using pointers to base classes will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public virtual bool Equals(Box? other) + { + // If parameter is null, return false. + if (Object.ReferenceEquals(other, null)) + { + return false; + } + + // Optimization for a common success case. + if (Object.ReferenceEquals(this, other)) + { + return true; + } + + // If run-time types are not exactly the same, return false. + if (this.GetType() != other.GetType()) + return false; + + // Note that the base class is not invoked because it is + // System.Object, which defines Equals as reference equality. + + // === Now, compare the fields which are specific to this class === + + if (Type != other.Type) + return false; + + if (ExtendedType != other.ExtendedType) + return false; + + if (ComputeSize() != other.ComputeSize()) + return false; + + if (!Children.SequenceEqual(other.Children)) + return false; + + if (Body != other.Body) + { + if (null == Body || null == other.Body) + return false; + + // If we reached this point, both Bodies are non-null and non-equal + if (Body.BaseStream.Length != other.Body.BaseStream.Length) + return false; + + // Same length. Compare the contents of the streams. Ignore different starting points, that is simply state. + long thisPosition = Body.BaseStream.Position; + long thatPosition = other.Body.BaseStream.Position; + + // Rewind streams + Body.BaseStream.Position = 0; + other.Body.BaseStream.Position = 0; + + long bytesTested = 0; + while (bytesTested < Body.BaseStream.Length) + { + int thisByte = Body.BaseStream.ReadByte(); + int thatByte = other.Body.BaseStream.ReadByte(); + + if (-1 == thisByte || -1 == thatByte) + { + // Stream stopped returning bytes before Length was reached, bail out + return (thisByte == thatByte); + } + + if (thisByte != thatByte) + { + // Found a difference between the streams + return false; + } + + bytesTested += 1; + } + + // If we reached this stage, the streams are equal. Restore their positions. + Body.BaseStream.Position = thisPosition; + other.Body.BaseStream.Position = thatPosition; + } + + // Ignore Dirty - that's merely a state and does not convey Box content + + // If we reach this point, the fields all match + return true; + } + + /// + /// Object.GetHashCode override. This must be done as a consequence of overriding + /// Object.Equals. + /// + /// Hash code which will be match the hash code of an object which is equal. + public override int GetHashCode() + { + return Type.GetHashCode(); // Only the Type of a Box is immutable + } + + /// + /// Override == operation (as recommended by MSDN). + /// + /// The box on the left-hand side of the ==. + /// The box on the right-hand side of the ==. + /// True if the two boxes are equal. + public static bool operator ==(Box? lhs, Box? rhs) + { + // Check for null on left side. + if (Object.ReferenceEquals(lhs, null)) + { + if (Object.ReferenceEquals(rhs, null)) + { + // null == null = true. + return true; + } + + // Only the left side is null. + return false; + } + // Equals handles case of null on right side. + return lhs.Equals(rhs); + } + + /// + /// Override != operation (as recommended by MSDN). + /// + /// The box on the left-hand side of the !=. + /// The box on the right-hand side of the !=. + /// True if the two boxes are equal. + public static bool operator !=(Box? lhs, Box? rhs) + { + return !(lhs == rhs); + } + + #endregion + } +} diff --git a/fmp4/BoxExtensions.cs b/fmp4/BoxExtensions.cs new file mode 100644 index 0000000..305e87a --- /dev/null +++ b/fmp4/BoxExtensions.cs @@ -0,0 +1,69 @@ + +using System.Text; + +namespace AMSMigrate.Fmp4 +{ + /// + /// This class contains Box extension methods. + /// + public static class BoxExtensions + { + /// + /// Clone (deep copy) the given box type by serializing and deserializing. + /// This is the non-generic method which is used when you don't know ahead of time + /// what the box is going to be. + /// Note that this method has side effects on the given box.Dirty flag (will go false). + /// + /// The box type. + /// The box instance to round-trip. Note that as a result of serialization, + /// the box.Dirty flag will become false. + /// The cloned box. + public static Box CloneBySerialization(this Box box) + { + return CloneBySerialization(box, parseBox: (reader) => MP4BoxFactory.ParseSingleBox(reader)); + } + + /// + /// Clone (deep copy) the given box type by serializing and deserializing. + /// This is the generic method which is used when you know ahead of time what the + /// box is going to be. If your prediction is wrong, an MP4DeserializeException will be thrown. + /// Note that this method has side effects on the given box.Dirty flag (will go false). + /// + /// The box type. + /// The box instance to round-trip. Note that as a result of serialization, + /// the box.Dirty flag will become false. + /// The cloned box. + public static BoxT CloneBySerialization(this BoxT box) where BoxT : Box + { + return CloneBySerialization(box, parseBox: (reader) => MP4BoxFactory.ParseSingleBox(reader)); + } + + /// + /// Clone (deep copy) the given box type by serializing and deserializing. + /// This is the generic method which is used when you know ahead of time what the + /// box is going to be. If your prediction is wrong, an MP4DeserializeException will be thrown. + /// Note that this method has side effects on the given box.Dirty flag (will go false). + /// + /// The box type. + /// The box instance to round-trip. Note that as a result of serialization, + /// the box.Dirty flag will become false. + /// The function which will parse and return a BoxT using the given reader. + /// The cloned box. + private static BoxT CloneBySerialization(this BoxT box, Func parseBox) where BoxT : Box + { + using (var stream = new MemoryStream()) + using (var writer = new MP4Writer(stream, Encoding.Default, leaveOpen: true)) + using (var reader = new MP4Reader(stream, Encoding.Default, leaveOpen: true)) + { + box.WriteTo(writer); + stream.Position = 0; + BoxT result = parseBox(reader); + + // We get to choose our policy for trunCopy.Dirty: always true/false, or copy from source. + // At the moment, we choose always dirty, to reflect that it has never been serialized. + result.Dirty = true; + return result; + } + } + } +} diff --git a/fmp4/Fmp4Fragment.cs b/fmp4/Fmp4Fragment.cs new file mode 100644 index 0000000..e60b2bc --- /dev/null +++ b/fmp4/Fmp4Fragment.cs @@ -0,0 +1,600 @@ + +using System.Diagnostics; + +namespace AMSMigrate.Fmp4 +{ + public class Fmp4Fragment : IEquatable + { + /// + /// Construct a fragment object with moof and mdat. + /// + /// the fragment header + /// the fragment data + public Fmp4Fragment(moofBox moof, mdatBox mdat) + { + Header = moof; + Data = mdat; + } + + /// + /// Deep copy constructor. + /// + /// The Fmp4Fragment to deep-copy. + public Fmp4Fragment(Fmp4Fragment other) + { + Header = other.Header.CloneBySerialization(); + Data = new mdatBox(other.Data); // mdatBox has its own deep copy constructor + } + + /// + /// The moof header of the fragment. + /// + public moofBox Header { get; private set; } + + /// + /// The mdat for the fragment. + /// + public mdatBox Data { get; private set; } + + /// + /// Reads an MP4 fragment (moof + mdat) from the stream + /// + /// The stream to read from + /// an Fmp4Fragment object with moof and mdat + public static Fmp4Fragment Create(Stream stream) + { + MP4Reader reader = new MP4Reader(stream); + + moofBox moof = MP4BoxFactory.ParseSingleBox(reader); + mdatBox mdat = MP4BoxFactory.ParseSingleBox(reader); + return new Fmp4Fragment(moof, mdat); + } + + /// + /// Saves this MP4 to a stream and returns it (rewound to 0). + /// + /// True to use sample defaults (duration, size, flags), if possible. + /// If false, the header is written as-is. + /// This fragment, serialized to a stream, rewound to position 0. It is + /// the responsibility of the caller to Dispose this stream. + public Stream ToStream(bool setSampleDefaults = true) + { + var outStream = new MemoryStream(); + var writer = new MP4Writer(outStream); // Don't dispose, that will close the stream. + + WriteTo(writer, setSampleDefaults); + writer.Flush(); + + outStream.Position = 0; // Rewind stream + return outStream; + } + + /// + /// Get the data offset from byte 0 of moof to byte 0 of mdat/sample_data. + /// + /// + private long ComputeMdatByte0Offset() + { + return (long)(Header.ComputeSize() + Data.ComputeBaseSizeBox()); + } + + /// + /// Returns the current data offset as specified in the tfhd/base_offset + trun/data_offset. + /// + /// The current data offset as specified in tfhd/base_offset + trun/data_offset. + public long GetDataOffset() + { + long totalOffset = 0; // In the absence of any data offsets, default offset is byte 0 of moof, which is byte 0 of stream + + if (Header.Track.Header.BaseOffset.HasValue) + { + totalOffset += (long)Header.Track.Header.BaseOffset.Value; + } + + if (Header.Track.TrackRun.DataOffset.HasValue) + { + totalOffset += Header.Track.TrackRun.DataOffset.Value; + } + + return totalOffset; + } + + /// + /// Maximum number of tries to recompute data offset. This is marked as internal to + /// allow for deep unit testing. + /// + internal int _maxTries = 5; + + /// + /// Reset the data offset (tfhd/base_offset + trun/data_offset), + /// overwriting whatever was there before, to point to byte 0 of mdat/sample_data. + /// + private void ResetDataOffset() + { + // It may take more than one try to set data offset, because overwriting the + // previous values may cause a change in box sizes (see UT's - they exercise this). + int i = 0; + for ( ; i < _maxTries; i++) + { + // Server/ChannelSink and Dash operate counter to Smooth Spec v6.0 + // which states that tfhd/base_data_offset is relative to mdat and + // which fails to specify a method of specifying data offset in trun. + // Instead, they all use trun/data_offset, relative to moof byte 0. + // We shall do the same here, and in addition, we shall remove any + // existing tfhd/base_data_offset and overwrite to point to + // mdatBox.SampleData[0], as is done in ChannelSink and Server code. + + // Note that Smooth spec REQUIRES tfhd and trun to be present. Should + // this fail to be true, we will crash below. + Header.Track.Header.BaseOffset = null; + Header.Track.TrackRun.DataOffset = Convert.ToInt32(ComputeMdatByte0Offset()); + + if (ComputeMdatByte0Offset() == Header.Track.TrackRun.DataOffset.Value) + { + break; + } + } + + if (i == _maxTries) + { + throw new NotSupportedException(string.Format( + "Unable to recompute data offset after {0} tries: trun/data_offset = {1} but should be {2}!", + _maxTries, Header.Track.TrackRun.DataOffset!.Value, ComputeMdatByte0Offset())); + } + } + + /// + /// Serialize this fragment to disk. + /// + /// The MP4Writer to write to. + /// True to use sample defaults (duration, size, flags), if possible. + /// If false, the header is written as-is. + public void WriteTo(MP4Writer writer, bool setSampleDefaults = true) + { + if (setSampleDefaults) + { + SetSampleDefaults(); + } + + ResetDataOffset(); + Header.WriteTo(writer); + Data.WriteTo(writer); + } + + /// + /// When preparing a fragment for VOD, or modifying it, it is best to remove + /// any existing tfxd or tfrf boxes. + /// + public void RemoveTfxdTfrf() + { + Header.Track.RemoveChildren((trafChild) => + trafChild.ExtendedType == MP4BoxType.tfxd || trafChild.ExtendedType == MP4BoxType.tfrf); + + // If we change the size of the moof we should recompute data offset + ResetDataOffset(); + } + + /// + /// Computes the size of this fragment if it were to be serialized to a stream. + /// + /// The size of this fragment if it were to be serialized to a stream. + public UInt64 ComputeSize() + { + return Header.ComputeSize() + Data.ComputeSize(); + } + + /// + /// Returns the sdtp box of this fragment, or null if none is found. + /// + private sdtpBox? _sdtp + { + get + { + return Header.Track.Children.Where(b => b is sdtpBox).SingleOrDefault() as sdtpBox; // This box is optional + } + } + + /// + /// This method removes the sdtp box from this fragment. + /// When not using sdtp, we remove it, to avoid accidentally writing out an empty sdtp box. + /// + private void RemoveSdtp() + { + sdtpBox? sdtp = _sdtp; + if (sdtp != null) + { + sdtp.Entries.Clear(); // Not strictly necessary, but some of the UTs are looking for this + Header.Track.Children.Remove(sdtp); + } + } + + /// + /// Clears all samples from this fragment, including any previous sample defaults. + /// + public void ClearSamples() + { + trunBox trun = Header.Track.TrackRun; + trun.Entries.Clear(); + RemoveSdtp(); + Data.SampleData = null; + + // Delete all sample defaults + tfhdBox tfhd = Header.Track.Header; + tfhd.DefaultSampleDuration = null; + tfhd.DefaultSampleSize = null; + tfhd.DefaultSampleFlags = null; + trun.FirstSampleFlags = null; + } + + /// + /// Searches the given samples to see if there are any constant values + /// which could be represented by a single default value. Note that each + /// attribute will only report a constant value once a given minimum is + /// exceeded. The minimum is determined by bit savings: when constant versus + /// explicit (trunEntry) cost the same number of bits, then we choose the + /// more flexible explicit trunEntry method (non-default). + /// + /// The samples to search for constant values. + /// If two or more samples use a constant duration, + /// it is returned here, otherwise returns null. + /// If two or more sizes use a constant size, + /// it is returned here, otherwise returns null. + /// If three or more samples use constant flags, + /// they are returned here, otherwise returns null. + /// If defaultFlags returns a non-null value and + /// the first sample flags differ from the rest, then this is a non-null value + /// representing value of the first sample's flags. Otherwise, it is null. + private static void GetSampleDefaults(List samples, + out UInt32? defaultDuration, + out UInt32? defaultSize, + out UInt32? defaultFlags, + out UInt32? firstSampleFlags) + { + // Initialize outputs + defaultDuration = null; + defaultSize = null; + defaultFlags = null; + firstSampleFlags = null; + + // We only choose to use defaults if there is two or more samples. This is because + // there are no bit savings when using defaults with a single sample, so we would + // prefer the more simpler, explicit practice of using trunEntry. + if (samples.Count < 2) + return; + + Fmp4FragmentSample firstSample = samples[0]; + bool allDurationsAreSame = true; + bool allSizesAreSame = true; + bool allFlagsAreSameExFirst = true; + for (int i = 1; i < samples.Count; i++) + { + Fmp4FragmentSample sample = samples[i]; + + if (sample.Duration != firstSample.Duration) + { + allDurationsAreSame = false; + } + + if (sample.Size != firstSample.Size) + { + allSizesAreSame = false; + } + + if (i > 1) + { + // We compare third and subsequent sample flags with second sample + if (sample.Flags != samples[1].Flags) + { + allFlagsAreSameExFirst = false; + } + } + } + + if (allDurationsAreSame) + { + defaultDuration = firstSample.Duration; + } + + if (allSizesAreSame) + { + defaultSize = firstSample.Size; + } + + // For the same reason that we refuse to use defaults for a single sample, + // we also refuse to use default flags for two or fewer samples. + if (allFlagsAreSameExFirst && samples.Count >= 3) + { + defaultFlags = samples[1].Flags; + if (firstSample.Flags != defaultFlags) + { + firstSampleFlags = firstSample.Flags; + } + } + } + + /// + /// A fragment may contain samples which use a mix of default and explicit values for + /// duration, size and flags. For each of these three attributes, this method checks + /// if we can use a default value. If so, it removes all explicit values and replaces them + /// with a default value (saves space). If not, it removes usage of default values + /// and forces all samples to use explicit values. + /// True if any defaults were found, false if all samples are using explicit values. + /// + public bool SetSampleDefaults() + { + List samples = Samples.ToList(); + + UInt32? defaultDuration; + UInt32? defaultSize; + UInt32? defaultFlags; + UInt32? firstSampleFlags; + GetSampleDefaults(samples, out defaultDuration, out defaultSize, out defaultFlags, out firstSampleFlags); + + // We first make explicit or make default, each sample. We do this first to make use + // of existing defaults, before overwriting them. + for (int i = 0; i < samples.Count; i++) + { + Fmp4FragmentSample sample = samples[i]; + + if (defaultDuration.HasValue) + { + sample.TrunEntry.SampleDuration = null; // Use default + } + else + { + sample.TrunEntry.SampleDuration = sample.Duration; // Make explicit + } + + if (defaultSize.HasValue) + { + sample.TrunEntry.SampleSize = null; // Use default + } + else + { + sample.TrunEntry.SampleSize = sample.Size; // Make explicit + } + + if (defaultFlags.HasValue) + { + sample.TrunEntry.SampleFlags = null; // Use default + } + else + { + sample.TrunEntry.SampleFlags = sample.Flags; // Make explicit + } + } + + // Now that all samples are prepared, we no longer need to keep the old defaults in place. Overwrite them. + tfhdBox tfhd = Header.Track.Header; + trunBox trun = Header.Track.TrackRun; + tfhd.DefaultSampleDuration = defaultDuration; + tfhd.DefaultSampleSize = defaultSize; + tfhd.DefaultSampleFlags = defaultFlags; + trun.FirstSampleFlags = firstSampleFlags; + + return (defaultDuration.HasValue || defaultSize.HasValue || defaultFlags.HasValue); + } + + /// + /// Before rewriting the samples in the fragment, check the proposed samples for correctness. + /// + /// The list of samples to check for correctness. + private static void ValidateIncomingSamples(List samples) + { + for (int i = 0; i < samples.Count; i++) + { + Fmp4FragmentSample sample = samples[i]; + Fmp4FragmentSample firstSample = samples[0]; + + if (sample.Data != null && (uint)sample.Data.Count != sample.Size) + { + throw new InvalidDataException( + string.Format("Cannot add sample #{0}: Data.Length ({1}) does not match Size ({2})!", + i, sample.Data.Count, sample.Size)); + } + + // Validate all samples are consistent on null/non-null SDTP + if ((null == sample.SdtpEntry) != (null == firstSample.SdtpEntry)) + { + throw new InvalidDataException( + string.Format("SdtpEntry inconsistency: firstSample.SdtpEntry is {0}, but sample {1} has it {2}!", + (null == firstSample.SdtpEntry) ? "null" : "non-null", i, + (null == sample.SdtpEntry) ? "null" : "non-null")); + } + } + } + + /// + /// Deletes the existing samples of this Fmp4Fragment and sets the new samples + /// to be the given enumeration of samples. + /// + /// The new samples for this Fmp4Fragment. + public void SetSamples(IEnumerable samplesEnum) + { + // Create a deep copy of the source samples (which could be from this very Fmp4Fragment) BEFORE clearing samples. + List samples = samplesEnum.Select(s => new Fmp4FragmentSample(s)).ToList(); + + // Analyze incoming samples + ValidateIncomingSamples(samples); + + // Now that we have checked incoming for consistency, we can start making side effects + ClearSamples(); + + int cbNewData = samples.Sum(s => s.Data!.Count); + var newData = new byte[cbNewData]; + IList trunEntries = Header.Track.TrackRun.Entries; + + bool needSdtp = (samples.Count > 0 && null != samples[0].SdtpEntry); + IList? sdtpEntries; + if (needSdtp) + { + Header.Track.Children.Add(new sdtpBox()); // Any previous sdtpBox is gone (ClearSamples) + sdtpEntries = _sdtp!.Entries; + } + else + { + RemoveSdtp(); + sdtpEntries = null; + } + + // Copy the new samples over + int i = 0; + int currOffset = 0; + foreach (Fmp4FragmentSample sample in samples) + { + // Explicitly set duration, size and flags. sample.TrunEntry is a deep copy, + // free to modify and add to my table at will. + sample.TrunEntry.SampleDuration = sample.Duration; + sample.TrunEntry.SampleSize = sample.Size; + sample.TrunEntry.SampleFlags = sample.Flags; + trunEntries.Add(sample.TrunEntry); + + if (null != sdtpEntries) + { + Debug.Assert(null != sample.SdtpEntry); // Programmer error - previous checks should have prevented this + sdtpEntries.Add(sample.SdtpEntry); // sample.SdtpEntry is already deep copy, free to add to table directly. + } + + sample.Data!.CopyTo(newData, currOffset); + currOffset += sample.Data.Count; + + // Advance variables + i += 1; + } + + Data.SampleData = newData; + } + + /// + /// The number of samples currently in this fragment. + /// + public int SampleCount => Header.Track.TrackRun.Entries.Count; + + /// + /// An enumeration of the samples currently in this fragment. The properties of these samples + /// (TrunEntry, SdtpEntry, Data) point directly within the fragment, meaning that modifications + /// to the sample are reflected immediately in the fragment. If a larger change is desired, + /// such as addition or removal of samples or sample data bytes, use the SetSamples() method. + /// + public IEnumerable Samples + { + get + { + int currOffset = 0; // We just assume that the data starts at mdatBox.SampleData[0] + + trunBox trun = Header.Track.TrackRun; + IList trunEntries = trun.Entries; + int numSamples = trunEntries.Count; + + // We will provide sdtp if and only if it exists and matches trun entry count + IList? sdtpEntries = null; + if (null != _sdtp && _sdtp.Entries.Count == numSamples) + { + sdtpEntries = _sdtp.Entries; + } + + for (int i = 0; i < numSamples; i++) + { + sdtpEntry? thisSdtpEntry = (null == sdtpEntries) ? null : sdtpEntries[i]; + UInt32? firstSampleFlags = (0 == i) ? trun.FirstSampleFlags : null; + var sample = new Fmp4FragmentSample(Header.Track.Header, trunEntries[i], + Data.SampleData, currOffset, thisSdtpEntry, firstSampleFlags); + + yield return sample; + + // Update variables + currOffset += Convert.ToInt32(sample.Size); + } + } + } + + #region equality methods + + /// + /// Compare this box to another object. + /// + /// the object to compare equality against + /// + public override bool Equals(Object? other) + { + return this.Equals(other as Fmp4Fragment); + } + + /// + /// Returns the hascode of the object. + /// + /// + public override int GetHashCode() + { + return Header.GetHashCode() ^ Data.GetHashCode(); + } + + /// + /// Compare two Fmp4Fragment's for equality. + /// + /// other Fmp4Fragment to compare against + /// + public bool Equals(Fmp4Fragment? other) + { + // If parameter is null, return false. + if (Object.ReferenceEquals(other, null)) + { + return false; + } + + // Optimization for a common success case. + if (Object.ReferenceEquals(this, other)) + { + return true; + } + + // If run-time types are not exactly the same, return false. + if (this.GetType() != other.GetType()) + return false; + + // Note that the base class is not invoked because it is + // System.Object, which defines Equals as reference equality. + + // === Now, compare the fields which are specific to this class === + if (Header != other.Header) + return false; + + if (Data != other.Data) + return false; + + // If we reach this point, the fields all match + return true; + } + + /// + /// Compare two Fmp4Fragment objects for equality. + /// + /// left hand side of == + /// right hand side of == + /// + public static bool operator ==(Fmp4Fragment? lhs, Fmp4Fragment? rhs) + { + // Check for null on left side. + if (Object.ReferenceEquals(lhs, null)) + { + if (Object.ReferenceEquals(rhs, null)) + { + // null == null = true. + return true; + } + + // Only the left side is null. + return false; + } + return lhs.Equals(rhs); + } + + public static bool operator !=(Fmp4Fragment? lhs, Fmp4Fragment? rhs) + { + return !(lhs == rhs); + } + + #endregion + + } +} diff --git a/fmp4/Fmp4FragmentSample.cs b/fmp4/Fmp4FragmentSample.cs new file mode 100644 index 0000000..eeeb50f --- /dev/null +++ b/fmp4/Fmp4FragmentSample.cs @@ -0,0 +1,319 @@ + + +namespace AMSMigrate.Fmp4 +{ + /// + /// The data needed to interpret a sample in Fmp4Fragment are scattered across a number of boxes. + /// The sample size/duration/flags can be explicitly set in trunEntry, or defaulted in tfhd/trun (first sample flags only). + /// The purpose of this class is to handle these complexities so that the caller can query sample + /// size or duration without worrying about the details. For sample modification, this class returns + /// properties which, if modified, modify the underlying Fmp4Fragment entries directly, such that + /// serializing the Fmp4Fragment will contain the modifications. Because of this intuitive arrangement, + /// tradeoffs had to be made which restrict the range of modifications. It is not possible to resize + /// the sample data, and it is not possible to add SdtpEntry flags if the sdtp box was not already present, + /// because such changes cannot be immediately propagated to Fmp4Fragment. Such changes are therefore + /// only possible by creating the desired Fmp4FragmentSample enumeration with resized sample data and/or + /// sdtp flags, and calling Fmp4Fragment.SetSamples. + /// + public class Fmp4FragmentSample : IEquatable + { + /// + /// Public constructor. + /// + /// Pointer to tfhd (required). Used to read default values. The tfhd will not be modified. + /// Pointer to trunEntry (required). Used to read and write explicit values. + /// If this sample reflects an existing sample in Fmp4Fragment, this trunEntry should point to + /// the underlying trunEntry in Fmp4Fragment.Header.Track.TrackRun.Entries[i] so that user writes + /// go directly to the fragment and will be written on the next Fmp4Fragment.WriteTo. + /// Byte array containing the sample data. If this sample reflects an + /// existing sample in Fmp4Fragment, this should be Fmp4Fragment.Data.SampleData so that user + /// writes go directly into the fragment and will be written on next Fmp4Fragment.WriteTo. + /// The size of the sample is inferred from the given tfhd and trunEntry. + /// The offset to start from in sampleData. + /// Pointer to sdtpEntry + /// If this is the first sample, then this should be the value + /// of trunBox.FirstSampleFlags (which may be null). Otherwise this should be null. + public Fmp4FragmentSample(tfhdBox tfhd, trunEntry trunEntry, byte[]? sampleData, int offset, sdtpEntry? sdtpEntry, UInt32? firstSampleFlags) + { + _tfhd = tfhd; + _firstSampleFlags = firstSampleFlags; + _trunEntry = trunEntry; + _sdtpEntry = sdtpEntry; + + if (sampleData != null) + { + _arraySegment = new ArraySegment(sampleData, offset, (int)Size); + } + } + + /// + /// Default constructor. It is private because this class is immutable. + /// +#pragma warning disable CS8618 + private Fmp4FragmentSample() + { + } +#pragma warning restore CS8618 + + /// + /// Deep Copy constructor. + /// + /// + public Fmp4FragmentSample(Fmp4FragmentSample other) + { + _tfhd = new tfhdBox(other._tfhd); + _firstSampleFlags = other._firstSampleFlags; + _trunEntry = new trunEntry(other._trunEntry); + if (other._arraySegment != null) + { + _arraySegment = new ArraySegment(other._arraySegment.Value.ToArray()); + } + + if (other._sdtpEntry != null) + { + _sdtpEntry = new sdtpEntry(other._sdtpEntry.Value); + } + } + + /// + /// Private pointer to track header, for obtaining sample defaults + /// during a read (it is never written to). + /// + private tfhdBox _tfhd; + + /// + /// If this is the first sample, then this the value of trunBox.FirstSampleFlags (which can be null). + /// + private UInt32? _firstSampleFlags; + + /// + /// Private backing store for public TrunEntry property. + /// + private trunEntry _trunEntry; + + /// + /// The trunEntry for this sample. This property may be freely written to, + /// but when reading sample attributes (size, duration, flags), it is best + /// to use the corresponding property, which will substitute the default value + /// if trunEntry does not have the attribute. + /// + public trunEntry TrunEntry + { + get + { + return _trunEntry; + } + } + + /// + /// Private backing store for public SdtpEntry property. + /// + private sdtpEntry? _sdtpEntry; + + /// + /// The sdtpEntry for this sample. This is optional, and can be null. If null, and caller + /// wishes to add, then caller must resort to the Fmp4Fragment.SetSamples method and create + /// sdtpEntry for all samples. + /// + public sdtpEntry? SdtpEntry => _sdtpEntry; + + /// + /// An ArraySegment of the original larger mdatBox.SampleData. Using ArraySegment allows us to cheaply offer + /// in-place write capabilities which will modify mdatBox.SampleData directly, without allowing changes + /// to the number of bytes used in this sample. + /// + private ArraySegment? _arraySegment; + + /// + /// The sample data. The bytes may be modified and will change Fmp4Fragment.Data.SampleData directly, + /// however, the number of bytes may not be changed. To change that, the caller must call Fmp4Fragment.SetSamples. + /// + public IList? Data => _arraySegment as IList; + + /// + /// The sample duration, either from trunEntry or the default from tfhd. + /// + public UInt32 Duration + { + get + { + if (TrunEntry.SampleDuration.HasValue) // TrunEntry is not permitted to be null + { + return TrunEntry.SampleDuration.Value; + } + + if (_tfhd.DefaultSampleDuration.HasValue) // _tfhd is not permitted to be null + { + return _tfhd.DefaultSampleDuration.Value; + } + + throw new InvalidOperationException("Sample has no duration (neither explicit nor default)!"); + } + } + + /// + /// The sample size, either from trunEntry or the default from tfhd. + /// + public UInt32 Size + { + get + { + if (TrunEntry.SampleSize.HasValue) // TrunEntry is not permitted to be null + { + return TrunEntry.SampleSize.Value; + } + + if (_tfhd.DefaultSampleSize.HasValue) // _tfhd is not permitted to be null + { + return _tfhd.DefaultSampleSize.Value; + } + + throw new InvalidOperationException("Sample has no size (neither explicit nor default)!"); + } + } + + /// + /// The sample flags, either from trunEntry, trun.FirstSampleFlags, or tfhd. + /// + public UInt32 Flags + { + get + { + if (TrunEntry.SampleFlags.HasValue) // TrunEntry is not permitted to be null + { + return TrunEntry.SampleFlags.Value; + } + + if (_firstSampleFlags.HasValue) + { + return _firstSampleFlags.Value; + } + + if (_tfhd.DefaultSampleFlags.HasValue) // _tfhd is not permitted to be null + { + return _tfhd.DefaultSampleFlags.Value; + } + + // Some content, such as BigBuckFragBlob, have no flags (neither explicit nor default). + // Rather than throw an exception, as earlier versions did, just return 0. + return 0; + } + } + + #region Equality Methods + + //===================================================================== + // Equality Methods + // + //===================================================================== + + /// + /// Object.Equals override. + /// + /// The object to test equality against. + /// True if the this and the given object are equal. + public override bool Equals(Object? obj) + { + return this.Equals(obj as Fmp4FragmentSample); + } + + /// + /// Implements IEquatable(Fmp4FragmentSample). This function is virtual and it is expected that + /// derived classes will override it, so that programs which attempt to test equality + /// using pointers to base classes will can enjoy results from the fully derived + /// equality implementation. + /// + /// The entry to test equality against. + /// True if the this and the given entry are equal. + public virtual bool Equals(Fmp4FragmentSample? other) + { + // If parameter is null, return false. + if (Object.ReferenceEquals(other, null)) + { + return false; + } + + // Optimization for a common success case. + if (Object.ReferenceEquals(this, other)) + { + return true; + } + + // If run-time types are not exactly the same, return false. + if (this.GetType() != other.GetType()) + return false; + + // Compare fields + if (_tfhd != other._tfhd) + return false; + + if (_firstSampleFlags != other._firstSampleFlags) + return false; + + if (_trunEntry != other._trunEntry) + return false; + + if ((Data == null && other.Data != null) || (Data != null && other.Data == null)) + { + return false; + } + + if (Data != null && other.Data != null && false == Enumerable.SequenceEqual(Data, other.Data)) + return false; + + if (_sdtpEntry != other._sdtpEntry) + return false; + + // If we reach this point, the fields all match + return true; + } + + /// + /// Object.GetHashCode override. This must be done as a consequence of overridding + /// Object.Equals. + /// + /// Hash code which will be match the hash code of an object which is equal. + public override int GetHashCode() + { + return GetType().GetHashCode(); + } + + /// + /// Override == operation (as recommended by MSDN). + /// + /// The entry on the left-hand side of the ==. + /// The entry on the right-hand side of the ==. + /// True if the two entries are equal. + public static bool operator ==(Fmp4FragmentSample? lhs, Fmp4FragmentSample? rhs) + { + // Check for null on left side. + if (Object.ReferenceEquals(lhs, null)) + { + if (Object.ReferenceEquals(rhs, null)) + { + // null == null = true. + return true; + } + + // Only the left side is null. + return false; + } + // Equals handles case of null on right side. + return lhs.Equals(rhs); + } + + /// + /// Override != operation (as recommended by MSDN). + /// + /// The entry on the left-hand side of the !=. + /// The entry on the right-hand side of the !=. + /// True if the two entries are equal. + public static bool operator !=(Fmp4FragmentSample? lhs, Fmp4FragmentSample? rhs) + { + return !(lhs == rhs); + } + + #endregion + + } +} diff --git a/fmp4/Fmp4FragmentSampleExtensions.cs b/fmp4/Fmp4FragmentSampleExtensions.cs new file mode 100644 index 0000000..2985b09 --- /dev/null +++ b/fmp4/Fmp4FragmentSampleExtensions.cs @@ -0,0 +1,192 @@ + +namespace AMSMigrate.Fmp4 +{ + /// + /// Extensions for handling Fmp4FragmentSample enumerations. + /// + public static class Fmp4FragmentSampleExtensions + { + /// + /// This method loops the given sequence of samples until the desired number of samples is reached. + /// + /// A sequence a samples. + /// The number of samples desired. + /// The given sequence of samples, looped if necessary, so that the requested number + /// of samples is returned. + public static IEnumerable Loop(this IEnumerable samples, int numSamples) + { + int i = 0; + while (i < numSamples) + { + foreach (Fmp4FragmentSample sample in samples) + { + yield return sample; + i += 1; + + if (i >= numSamples) + { + break; + } + } + } + } + + /// + /// Given a sequence of unsigned integers, returns the count of these unsigned + /// integers where their sum is strictly greater than to the given threshold. + /// + /// A sequence of unsigned integers. + /// A threshold for the sequence to strictly exceed. + /// The number of unsigned integers whose sum strictly exceeds the given threshold, + /// or -1 if the unsigned integers do not exceed the threshold. Note that the return value + /// will never be zero. + public static int CountWhenSumGreaterThan(this IEnumerable addends, UInt64 threshold) + { + int count = 0; + UInt64 currentSum = 0; + foreach (UInt32 addend in addends) + { + count += 1; + currentSum += addend; + if (currentSum > threshold) + { + return count; + } + } + + // If we reach this point, we have not exceeded the threshold + return -1; + } + + /// + /// Sum of a sequence of UInt32's. + /// + /// An enumeration of UInt32's. + /// The sum of the sequence of UInt32's. + public static UInt64 Sum(this IEnumerable numbers) + { + UInt64 sum = 0; + foreach (UInt32 number in numbers) + { + sum += number; + } + return sum; + } + + /// + /// A method supplied by the caller to modify a sample. + /// + /// A copy-constructed Fmp4Fragment (deep TrunEntry/SdtpEntry copy, shallow Data copy). + /// The sum of sample durations up to but not including lastSampleCopy. + /// The number of samples up to but not including lastSampleCopy. + public delegate void ModifySample(Fmp4FragmentSample lastSampleCopy, long sumOfSampleDurationsExLast, int numSamplesExLast); + + /// + /// This method modifies a copy of the last sample, according to the supplied modifyLastSample method. + /// In other words, the incoming sequence is passed verbatim, but for the last sample, which is + /// deep-copied and modified before being returned. In this way, there are no side effects to the incoming + /// enumeration, unless the caller himself performs it, and even then, the last sample shall not be affected. + /// + /// An enumeration of samples. + /// A method which will be called to modify a deep copy of the last sample. + /// An enumeration of samples with the last sample deep-copied and modified. + public static IEnumerable ModifyLastSample(this IEnumerable samples, + ModifySample modifyLastSample) + { + if (null == samples) + { + throw new ArgumentNullException(nameof(samples)); + } + + if (null == modifyLastSample) + { + throw new ArgumentNullException(nameof(modifyLastSample)); + } + + int numSamplesExLast = 0; + long sumOfSampleDurationsExLast = 0; + Fmp4FragmentSample? prevSample = null; + foreach (Fmp4FragmentSample sample in samples) + { + if (null != prevSample) + { + sumOfSampleDurationsExLast += prevSample.Duration; + numSamplesExLast += 1; + yield return prevSample; + } + + prevSample = sample; + } + + // Here we are, on the last sample + if (null != prevSample) + { + Fmp4FragmentSample lastSampleCopy = new Fmp4FragmentSample(prevSample); + modifyLastSample(lastSampleCopy, sumOfSampleDurationsExLast, numSamplesExLast); + yield return lastSampleCopy; + } + } + + /// + /// This method normalizes the last sample duration. It does so by averaging the durations + /// of all samples except for the last one, then overwriting the last sample's duration with + /// that average. If the input seqeuence only has a single sample, then no normalization + /// takes place. The last sample is still deep-copied, but the duration remains unchanged. + /// + /// An enumeration of samples. This enumeration shall not be modified + /// by the actions of this method (no side effects from calling this method). + /// An enumeration of samples, with the last sample's duration replaced with the + /// normalized (average) duration. This last sample is a deep copy and so the act of + /// normalizing the last duration has no side effects on the original sequence. + /// If the input seqeuence only has a single sample, then no normalization + /// takes place. The last sample is still deep-copied, but the duration remains unchanged. + public static IEnumerable NormalizeLastSample(this IEnumerable samples) + { + return samples.ModifyLastSample(modifyLastSample: + (lastSampleCopy, sumOfSampleDurationsExLast, numSamplesExLast) => + { + // If no other samples, we can't normalize, so don't + if (numSamplesExLast > 0) + { + long normalizedDuration = (sumOfSampleDurationsExLast + numSamplesExLast / 2) / numSamplesExLast; + lastSampleCopy.TrunEntry.SampleDuration = Convert.ToUInt32(normalizedDuration); + } + }); + } + + /// + /// This method pads the last sample duration so that the sum of all sample durations + /// is equal to the desired total duration. + /// + /// An enumeration of samples. This enumeration shall not be modified + /// by the actions of this method (no side effects from calling this method). + /// The desired total duration of all the durations in samples. + /// If the durations do not already sum to totalDuration, then the last sample will be modified + /// to achieve the desired sum of totalDuration. + /// An enumeration of samples, with the last sample's duration replaced with the + /// padded duration to make the sum of totalDuration. This last sample is a deep copy and so the act of + /// normalizing the last duration has no side effects on the original sequence. + public static IEnumerable PadLastSample(this IEnumerable samples, + long totalDuration) + { + if (totalDuration < 0) + { + throw new ArgumentOutOfRangeException(nameof(totalDuration), "Cannot be less than zero."); + } + + return samples.ModifyLastSample(modifyLastSample: + (lastSampleCopy, sumOfSampleDurationsExLast, numSamplesExLast) => + { + long padDuration = totalDuration - sumOfSampleDurationsExLast; + if (padDuration <= 0) + { + throw new InvalidOperationException( + String.Format("Unable to pad to totalDuration ({0}), sum of {1} sample durations has already met or exceeded it ({2})!", + totalDuration, numSamplesExLast, sumOfSampleDurationsExLast)); + } + + lastSampleCopy.TrunEntry.SampleDuration = Convert.ToUInt32(padDuration); + }); + } + } +} diff --git a/fmp4/FullBox.cs b/fmp4/FullBox.cs new file mode 100644 index 0000000..a821f6c --- /dev/null +++ b/fmp4/FullBox.cs @@ -0,0 +1,297 @@ + +using System.Globalization; +using System.Diagnostics; + +namespace AMSMigrate.Fmp4 +{ + /// + /// Implements the FullBox object as defined in 14496-12. + /// + public class FullBox : Box, IEquatable + { + /// + /// Constructor for compact type. Only used by derived classes. + /// + /// The version of this FullBox. + /// The flags for this FullBox. + /// The compact type. + protected FullBox(Byte version, + UInt32 flags, + UInt32 boxtype) + : base(boxtype) + { + Version = version; + Flags = flags; + } + + /// + /// Constructor for extended type ('uuid' box). Only used by derived classes. + /// + /// The version of this FullBox. + /// The flags for this FullBox. + /// The extended type. + protected FullBox(Byte version, + UInt32 flags, + Guid extendedType) + : base(extendedType) + { + Version = version; + Flags = flags; + } + + /// + /// Deserializing constructor. + /// + /// The box which contains the bytes we should deserialize from. + protected FullBox(Box box) + : base(box) + { + } + + /// + /// Parse the body which includes the version and flags. + /// + protected override void ReadBody() + { + base.ReadBody(); + long startPosition = Body!.BaseStream.Position; + try + { + UInt32 dw = Body.ReadUInt32(); + _version = (Byte)(dw >> 24); + _flags = (dw & 0x00FFFFFF); + } + catch (EndOfStreamException ex) + { + // Reported error offset will point to start of box + throw new MP4DeserializeException(TypeDescription, -BodyPreBytes, BodyInitialOffset, + String.Format(CultureInfo.InvariantCulture, "Could not read Version and Flags for FullBox, only {0} bytes left in reported size, expected {1}", + Body.BaseStream.Length - startPosition, ComputeLocalSize()), ex); + } + + } + + /// + /// Calculate the current size of this box. + /// + /// The current size of this box, if it were to be written to disk now. + public override UInt64 ComputeSize() + { + UInt64 thisSize = ComputeLocalSize(); + UInt64 baseSize = base.ComputeSize(); + + return thisSize + baseSize; + } + + /// + /// Calculates the current size of just this class (base classes excluded). This is called by + /// ComputeSize(). + /// + /// The current size of just the fields from this box. + protected static new UInt64 ComputeLocalSize() + { + return 4; // bVersion + dwFlags(24bit) + } + + /// + /// This function does the actual work of serializing this class to disk. + /// + /// The MP4Writer to write to. + protected override void WriteToInternal(MP4Writer writer) + { + base.WriteToInternal(writer); + + UInt64 thisSize = ComputeLocalSize(); + long startPosition = writer.BaseStream.Position; + UInt32 versionAndFlags = ((UInt32)Version << 24) | (Flags & 0x00FFFFFF); + writer.WriteUInt32(versionAndFlags); + + // Confirm that we wrote exactly the number of bytes we said we would + Debug.Assert(writer.BaseStream.Position - startPosition == (long)thisSize || + Stream.Null == writer.BaseStream); + } + + + /// + /// Backing store for Version. + /// + private Byte _version; + + /// + /// The version field of this FullBox, as per 14496-12. + /// + public Byte Version + { + get + { + return _version; + } + protected set + { + _version = value; + SetDirty(); + } + } + + /// + /// Backing store for Flags. + /// + private UInt32 _flags; + + /// + /// The flags field of this FullBox, as per 14496-12. + /// + public UInt32 Flags + { + get + { + return _flags; + } + protected set + { + // Check if flags fit within 24 bits + if (0 != (value & ~0x00FFFFFF)) + { + throw new ArgumentOutOfRangeException(nameof(value), "FullBox flags must fit within 24 bits"); + } + + _flags = value; + SetDirty(); + } + } + + #region Equality Methods + + //===================================================================== + // Equality Methods + // + // In order to implement IEquatable the way in which MSDN recommends, + // it is unfortunately necessary to cut-and-paste this group of functions + // into each derived class and to do a search-and-replace on the types + // to match the derived class type. Generic classes do not work because + // of the special rules around operator overloading and such. + // + // In addition to this cut-and-paste, search-and-replace, the derived + // class should also override the Equals(base) method so that programs + // which attempt to test equality using pointers to base classes will + // be seeing results from the fully derived equality implementations. + // + // In other words, Box box = new DerivedBox(), box.Equals(box2). If + // Equals(base) is not overriden, then the base implementation of + // Equals(base) is used, which will not compare derived fields. + //===================================================================== + + /// + /// Object.Equals override. + /// + /// The object to test equality against. + /// True if the this and the given object are equal. + public override bool Equals(Object? other) + { + return this.Equals(other as FullBox); + } + + /// + /// Box.Equals override. This is done so that programs which attempt to test equality + /// using pointers to Box will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public override bool Equals(Box? other) + { + return this.Equals(other as FullBox); + } + + /// + /// Implements IEquatable(FullBox). This function is virtual and it is expected that + /// derived classes will override it, so that programs which attempt to test equality + /// using pointers to base classes will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public virtual bool Equals(FullBox? other) + { + // If parameter is null, return false. + if (Object.ReferenceEquals(other, null)) + { + return false; + } + + // Optimization for a common success case. + if (Object.ReferenceEquals(this, other)) + { + return true; + } + + // If run-time types are not exactly the same, return false. + if (this.GetType() != other.GetType()) + return false; + + // === First, check if base classes are equal === + + if (false == base.Equals(other)) + return false; + + // === Now, compare the fields which are specific to this class === + + if (Version != other.Version) + return false; + + if (Flags != other.Flags) + return false; + + // If we reach this point, the fields all match + return true; + } + + /// + /// Object.GetHashCode override. This must be done as a consequence of overridding + /// Object.Equals. + /// + /// Hash code which will be match the hash code of an object which is equal. + public override int GetHashCode() + { + return base.GetHashCode(); // All FullBox fields are mutable + } + + /// + /// Override == operation (as recommended by MSDN). + /// + /// The box on the left-hand side of the ==. + /// The box on the right-hand side of the ==. + /// True if the two boxes are equal. + public static bool operator ==(FullBox? lhs, FullBox? rhs) + { + // Check for null on left side. + if (Object.ReferenceEquals(lhs, null)) + { + if (Object.ReferenceEquals(rhs, null)) + { + // null == null = true. + return true; + } + + // Only the left side is null. + return false; + } + // Equals handles case of null on right side. + return lhs.Equals(rhs); + } + + /// + /// Override != operation (as recommended by MSDN). + /// + /// The box on the left-hand side of the !=. + /// The box on the right-hand side of the !=. + /// True if the two boxes are equal. + public static bool operator !=(FullBox? lhs, FullBox? rhs) + { + return !(lhs == rhs); + } + + #endregion + + } +} diff --git a/fmp4/MP4BoxFactory.cs b/fmp4/MP4BoxFactory.cs new file mode 100644 index 0000000..dc118e9 --- /dev/null +++ b/fmp4/MP4BoxFactory.cs @@ -0,0 +1,123 @@ + +using System.Diagnostics; +using System.Globalization; +using System.Reflection; + +namespace AMSMigrate.Fmp4 +{ + /// + /// This class creates MP4 boxes from the given stream. + /// + public static class MP4BoxFactory + { + /// + /// This function parses and returns a single MP4 box from the given stream. It should be + /// noted that some MP4 boxes, such as 'moof', are container boxes and actually contain many + /// other boxes. Thus in those cases, "single" MP4 box refers to the single 'moof' box returned, + /// but in fact many boxes were parsed and returned in the course of returning the single 'moof'. + /// + /// The MP4Reader to read from. + /// The file offset which corresponds with + /// reader.BaseStream.Position == 0. This is used to get the correct file offset in error messages. + /// The next box in the stream. + public static Box ParseSingleBox(MP4Reader reader, long initialOffset = 0) + { + // First, parse the Box to get type and size + Box box = new Box(reader, initialOffset); + + // Next, figure out what type of box this is + Type? boxType; + bool found; + + if (MP4BoxType.uuid == box.Type) + { + found = MP4BoxType.ExtendedType.TryGetValue(box.ExtendedType!.Value, out boxType); + } + else + { + found = MP4BoxType.CompactType.TryGetValue(box.Type, out boxType); + } + + if (found && boxType != null) + { + // We know what kind of object represents this box. Call that object's deserializing constructor. + object[] ctorParams = new object[] { box }; + Type[] ctorParamTypes = new Type[] { typeof(Box) }; + ConstructorInfo? ctor = boxType.GetConstructor(ctorParamTypes); + Debug.Assert(null != ctor); // Notify programmer that the box was registered, but could not find the constructor + if (null != ctor) + { + var regenedBox = ctor.Invoke(ctorParams) as Box; + box = regenedBox!; + } + } + + return box; + } + + /// + /// This function parses and returns a specific MP4 box from the given stream. It should be + /// noted that some MP4 boxes, such as 'moof', are container boxes and actually contain many + /// other boxes. Thus in those cases, "single" MP4 box refers to the single 'moof' box returned, + /// but in fact many boxes were parsed and returned in the course of returning the single 'moof'. + /// + /// This function should be called when it is known exactly what the next box WILL be. If the + /// specified box is not found to be the very next box, then an exception is thrown. This is all + /// done in the name of security. The act of identifying the box exposes very little for an attacker + /// to work with, while the act of fully deserializing exposes much more. By calling this function, + /// we choose to fully deserialize only if the next box matches the requested type. + /// + /// The MP4Reader to read from. + /// The file offset which corresponds with + /// reader.BaseStream.Position == 0. This is used to get the correct file offset in error messages. + /// The next box in the stream, IF it is the specific type requested. + public static T ParseSingleBox(MP4Reader reader, long initialOffset = 0) where T : Box + { + long startPosition = reader.BaseStream.Position; + + UInt32 size; + UInt32 type; + Box.PeekCompactType(out size, out type, reader, initialOffset); + + // Next, figure out what type of box this is + Type? boxType; + bool found; + if (MP4BoxType.uuid == type) + { + throw new NotImplementedException("Cannot PeekExtendedType yet."); + } + else + { + found = MP4BoxType.CompactType.TryGetValue(type, out boxType); + } + + if (found) + { + if (boxType == typeof(T)) + { + var singleBox = ParseSingleBox(reader, initialOffset); + var targetBox = singleBox as T; + + return targetBox!; + } + } + + // If we reached this stage, this box is NOT the box the caller was expecting. + // Find out the name of caller's requested type, for the error message + var requestedBoxTypeDescription = typeof(T).FullName!; + foreach (KeyValuePair entry in MP4BoxType.CompactType) + { + if (entry.Value == typeof(T)) + { + requestedBoxTypeDescription = Box.CompactTypeToString(entry.Key); + } + } + + // Offset should point to start of box + throw new MP4DeserializeException(requestedBoxTypeDescription, startPosition, initialOffset, + String.Format(CultureInfo.InvariantCulture, "Instead of expected box, found a box of type {0}!", + Box.CompactTypeToString(type))); + } + + } +} diff --git a/fmp4/MP4BoxType.cs b/fmp4/MP4BoxType.cs new file mode 100644 index 0000000..dd09c9e --- /dev/null +++ b/fmp4/MP4BoxType.cs @@ -0,0 +1,105 @@ + +using System.Globalization; + +namespace AMSMigrate.Fmp4 +{ + /// + /// This class stores the known box types (both compact boxtype and extended), + /// and keeps a lookup table of the corresponding object type which understands that + /// box type. + /// + public static class MP4BoxType + { + //=================================================================== + // Known Box Types (Compact) + //=================================================================== + public const UInt32 uuid = 0x75756964; // 'uuid' + public const UInt32 moof = 0x6D6F6F66; // 'moof' + public const UInt32 mfhd = 0x6D666864; // 'mfhd' + public const UInt32 traf = 0x74726166; // 'traf' + public const UInt32 tfhd = 0x74666864; // 'tfhd' + public const UInt32 trun = 0x7472756E; // 'trun' + public const UInt32 tfdt = 0x74666474; // 'tfdt' + public const UInt32 sdtp = 0x73647470; // 'sdtp' + public const UInt32 mdat = 0x6D646174; // 'mdat' + public const UInt32 moov = 0x6D6F6F76; // 'moov' + public const UInt32 mvhd = 0x6D766864; // 'mvhd' + public const UInt32 trak = 0x7472616B; // 'trak' + public const UInt32 tkhd = 0x746B6864; // 'tkhd' + public const UInt32 mvex = 0x6D766578; // 'mvex' + public const UInt32 trex = 0x74726578; // 'trex' + public const UInt32 mdia = 0x6D646961; // 'mdia' + public const UInt32 hdlr = 0x68646C72; // 'hdlr' + public const UInt32 mdhd = 0x6D646864; // 'mdhd' + + + //=================================================================== + // Known Box Types (Extended 'uuid') + //=================================================================== + public static readonly Guid tfxd = new Guid("6d1d9b05-42d5-44e6-80e2-141daff757b2"); + public static readonly Guid tfrf = new Guid("d4807ef2-ca39-4695-8e54-26cb9e46a79f"); + public static readonly Guid c2pa = new Guid("d8fec3d6-1b0e-483c-9297-5828877ec481"); + + //=================================================================== + // Registered Box Objects (Compact Type) + //=================================================================== + private static Dictionary _compactType = new Dictionary() + { + { moof, typeof(moofBox) }, + { mfhd, typeof(mfhdBox) }, + { traf, typeof(trafBox) }, + { tfhd, typeof(tfhdBox) }, + { trun, typeof(trunBox) }, + { tfdt, typeof(tfdtBox) }, + { sdtp, typeof(sdtpBox) }, + { moov, typeof(moovBox) }, + { mvhd, typeof(mvhdBox) }, + { trak, typeof(trakBox) }, + { tkhd, typeof(tkhdBox) }, + { mvex, typeof(mvexBox) }, + { trex, typeof(trexBox) }, + { mdia, typeof(mdiaBox) }, + { hdlr, typeof(hdlrBox) }, + { mdhd, typeof(mdhdBox) }, + { mdat, typeof(mdatBox) }, + }; + + public static Dictionary CompactType + { + get + { + return _compactType; + } + } + + //=================================================================== + // Registered Box Objects (Extended 'uuid' Type) + //=================================================================== + private static Dictionary _extendedType = new Dictionary(); + + public static Dictionary ExtendedType + { + get + { + return _extendedType; + } + } + + /// + /// Helper function to convert a string type to its integer value. + /// + /// The string representation of the type of an mp4 box e.g: "moof" + /// + public static UInt32 StringToCompactType(string type) + { + if (type.Length != 4) + { + throw new ArgumentException( + String.Format(CultureInfo.InvariantCulture, "Invalid box type {0}. Box type must have only 4 characters", type)); + } + //combine the ASCII values of each character to form the 32 bit type. + UInt32 value = type.Aggregate(0, (w, c) => ((w << 8) + (byte)c)); + return value; + } + } +} diff --git a/fmp4/MP4DeserializeException.cs b/fmp4/MP4DeserializeException.cs new file mode 100644 index 0000000..d191d56 --- /dev/null +++ b/fmp4/MP4DeserializeException.cs @@ -0,0 +1,103 @@ + +using System.Globalization; +using System.Runtime.Serialization; + +namespace AMSMigrate.Fmp4 +{ + /// + /// This class is used to report errors which have occurred during MP4 deserialization. + /// + [Serializable] + public class MP4DeserializeException : Exception + { + /// + /// Empty implementation to satisfy CA1032 (Implement standard exception constructors). + /// + public MP4DeserializeException() + : base() + { + } + + /// + /// Empty implementation to satisfy CA1032 (Implement standard exception constructors). + /// + public MP4DeserializeException(string message) + : base(message) + { + } + + /// + /// Empty implementation to satisfy CA1032 (Implement standard exception constructors). + /// + public MP4DeserializeException(string message, Exception ex) + : base(message, ex) + { + } + + /// + /// Creates an MP4DeserializeException. + /// + /// Example error message: + /// + /// "Error deserializing 'trun' at offset 512: Could not read Version and Flags for FullBox, + /// only 3 bytes left in reported size, expected 4" + /// + /// + /// The description of the box type which could not be + /// deserialized, eg. 'trun'. + /// The offset to add to initialOffset to produce the final + /// offset. Can be negative. In general, the expected value here is the value which will + /// produce the offset to the start of the current box. In other words, if an error occurred + /// while deserializing the version/flags for FullBox, the offset we report should point + /// to the start of the current Box, ie. should point to the MSB of the Box.Size field. + /// Typically the initialOffset of the stream currently being + /// read from. This is simply added to relativeOffset to make a single number. + /// The error message. + public MP4DeserializeException(string boxTypeDescription, + long relativeOffset, + long initialOffset, + String message) + : base(String.Format(CultureInfo.InvariantCulture, "Error deserializing {0} at offset {1}: {2}", + boxTypeDescription, initialOffset + relativeOffset, message)) + { + } + + /// + /// Creates an MP4DeserializeException. + /// + /// Example error message: + /// + /// "Error deserializing 'trun' at offset 512: Could not read Version and Flags for FullBox, + /// only 3 bytes left in reported size, expected 4" + /// + /// + /// The description of the box type which could not be + /// deserialized, eg. 'trun'. + /// The offset to add to initialOffset to produce the final + /// offset. Can be negative. In general, the expected value here is the value which will + /// produce the offset to the start of the current box. In other words, if an error occurred + /// while deserializing the version/flags for FullBox, the offset we report should point + /// to the start of the current Box, ie. should point to the MSB of the Box.Size field. + /// Typically the initialOffset of the stream currently being + /// read from. This is simply added to relativeOffset to make a single number. + /// The error message. + /// The exception which caused this MP4DeserializeException. + public MP4DeserializeException(string boxTypeDescription, + long relativeOffset, + long initialOffset, + String message, + Exception innerException) + : base(String.Format(CultureInfo.InvariantCulture, "Error deserializing {0} at offset {1}: {2}", + boxTypeDescription, initialOffset + relativeOffset, message), innerException) + { + } + + /// + /// Empty implementation to satisfy CA1032 (Implement standard exception constructors). + /// + protected MP4DeserializeException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/fmp4/MP4Reader.cs b/fmp4/MP4Reader.cs new file mode 100644 index 0000000..36d8862 --- /dev/null +++ b/fmp4/MP4Reader.cs @@ -0,0 +1,132 @@ + +using System.Net; +using System.Text; + +namespace AMSMigrate.Fmp4 +{ + /// + /// A utility class the extends BinaryReader and writes values in big endian format for MP4. + /// + public class MP4Reader : BinaryReader + { + /// + /// Construct with a given stream. + /// + /// the stream to read from + public MP4Reader(Stream stream) : + base(stream) + { + } + + /// + /// Construct with a given stream. + /// + /// the stream to read from + /// The character encoding to use. + /// True to leave the stream open after MP4Reader object is disposed. Otherwise, false. + public MP4Reader(Stream stream, Encoding encoding, bool leaveOpen) : + base(stream, encoding, leaveOpen) + { + } + + /// + /// Reads a signed integer as big endian from the stream. + /// + /// 16 bit integer value + public override Int16 ReadInt16() + { + return IPAddress.NetworkToHostOrder(base.ReadInt16()); + } + + /// + /// Reads an unsigned 16 bit integer in big endian format from the stream. + /// + /// + public override UInt16 ReadUInt16() + { + return (UInt16)this.ReadInt16(); + } + + /// + /// Reads a singed 32 bit integer value in big endian format from stream. + /// + /// A big-endian signed 32-bit integer from bit stream + public override Int32 ReadInt32() + { + return IPAddress.NetworkToHostOrder(base.ReadInt32()); + } + + /// + /// Helper function to read unsigned 32-bit integer in BIG-ENDIAN format from a stream. + /// + /// A big-endian unsigned 32-integer from the bitstream. + public override UInt32 ReadUInt32() + { + return (UInt32)this.ReadInt32(); + } + + /// + /// Reads a 64-bit signed integer in big endian format from a stream. + /// + /// A big endian 64 bit integer + public override Int64 ReadInt64() + { + return IPAddress.NetworkToHostOrder(base.ReadInt64()); + } + + /// + /// Helper function to read unsigned 64-bit integer in BIG-ENDIAN format from a stream. + /// + /// An big-endian unsigned 64-integer from the bitstream. + public override UInt64 ReadUInt64() + { + return (UInt64)this.ReadInt64(); + } + + + /// + /// Read a GUID value from the mp4 byte stream. + /// + /// A Guid object + public Guid ReadGuid() + { + Int32 data1 = ReadInt32(); + Int16 data2 = ReadInt16(); + Int16 data3 = ReadInt16(); + Byte[] data4 = ReadBytes(8); + + return new Guid(data1, data2, data3, data4); + } + + + /// + /// This function reads an 8/16/24/32-bit big-endian field from disk. + /// + /// Returns the value here. + /// The size of the field (8/16/24/32). + public UInt32 ReadVariableLengthField(int bitDepth) + { + UInt32 value = 0; + + if (bitDepth % 8 != 0) + { + throw new ArgumentException("bitDepth must be multiple of 8"); + } + + if (bitDepth > 32) + { + throw new ArgumentException("bitDepth must be 8/16/24/32 only"); + } + + for (int i = 0; i < bitDepth; i += 8) + { + value <<= 8; + value |= ReadByte(); + } + + return value; + } + + + } +} diff --git a/fmp4/MP4Writer.cs b/fmp4/MP4Writer.cs new file mode 100644 index 0000000..4997705 --- /dev/null +++ b/fmp4/MP4Writer.cs @@ -0,0 +1,136 @@ + +using System.Diagnostics; +using System.Net; +using System.Text; + +namespace AMSMigrate.Fmp4 +{ + /// + /// Implements a writer class that writes data in big endian format for MP4 boxes. + /// + public class MP4Writer : BinaryWriter + { + /// + /// Construct an MP4 writer that writes to the given stream. + /// + /// the stream to write to + public MP4Writer(Stream stream) : + base(stream) + { + } + + /// + /// Construct an MP4 writer that writes to the given stream. + /// + /// the stream to write to + /// The character encoding to use. + /// True to leave the stream open after MP4Reader object is disposed. Otherwise, false. + public MP4Writer(Stream stream, Encoding encoding, bool leaveOpen) : + base(stream, encoding, leaveOpen) + { + } + + #region signed writes + + /// + /// Write an 16 bit integer in big endian format to the stream. + /// + /// the value to write. + public void WriteInt16(Int16 value) + { + base.Write(IPAddress.HostToNetworkOrder(value)); + } + + /// + /// Writes a 32 bit integer in big endian format to the stream + /// + /// the value to write. + public void WriteInt32(Int32 value) + { + base.Write(IPAddress.HostToNetworkOrder(value)); + } + + /// + /// Writes a 64 bit signed integer in big endian format to the stream + /// + /// The value to write. + public void WriteInt64(Int64 value) + { + base.Write(IPAddress.HostToNetworkOrder(value)); + } + + #endregion + + #region unsigned writes + + /// + /// Writes a unsigned 16 bit integer + /// + /// The value to write + public void WriteUInt16(UInt16 value) + { + this.WriteInt16((Int16)value); + } + + /// + /// Writes an unsigned 32 bit integer in big endian format. + /// + /// The value to write + public void WriteUInt32(UInt32 value) + { + this.WriteInt32((Int32)value); + } + + /// + /// Writes an unsigned 64 bit integer in big endian format. + /// + /// The value to write. + public void WriteUInt64(UInt64 value) + { + this.WriteInt64((Int64)value); + } + + #endregion + + + /// + /// This function writes an 8/16/24/32-bit big-endian field to disk. + /// + /// The MP4Writer to write to. + /// The value to write. + /// The size of the field on disk (8/16/24/32). + public void WriteVariableLengthField(UInt64 value, int bitDepth) + { + if (bitDepth % 8 != 0) + { + throw new ArgumentException("bitDepth must be a multiple of 8"); + } + + if (bitDepth > 32) + { + throw new ArgumentException("bitDepth must be less than or equal to 32"); + } + + for (int i = 0; i < bitDepth; i += 8) + { + Byte output = (Byte)(value >> (bitDepth - 8 - i)); + Write(output); + } + } + + /// + /// Write a GUID Value in the big endian order. + /// + /// + public void Write(Guid value) + { + Byte[] guid = value.ToByteArray(); + Debug.Assert(16 == guid.Length); + WriteInt32(BitConverter.ToInt32(guid, 0)); + WriteInt16(BitConverter.ToInt16(guid, 4)); + WriteInt16(BitConverter.ToInt16(guid, 6)); + Write(guid, 8, 8); + } + + } +} diff --git a/fmp4/VariableLengthField.cs b/fmp4/VariableLengthField.cs new file mode 100644 index 0000000..64ef34d --- /dev/null +++ b/fmp4/VariableLengthField.cs @@ -0,0 +1,325 @@ + +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Globalization; + +namespace AMSMigrate.Fmp4 +{ + /// + /// This class allows us to deal with variable-length fields as specified in some + /// ISO 14496-12 boxes, such as Box.Size, 'trun' entries such as sample_duration, sample_size, etc. + /// + [DebuggerDisplay("Value = {_value}, BitDepth = {_bitDepth}")] + public class VariableLengthField + { + /// + /// Public constructor. + /// + /// A sorted integer array of valid bit depths for this field. + /// The initial value to use for this field. + public VariableLengthField(int[] validBitDepths, UInt64 initialValue) + { + // Confirm that validBitDepths is sorted + for (int i = 1; i < validBitDepths.Length; i++) + { + if (validBitDepths[i - 1] > validBitDepths[i]) + { + throw new ArgumentException("validBitDepths array must be sorted!", nameof(validBitDepths)); + } + } + + _validBitDepths = new List(validBitDepths); + BitDepth = validBitDepths[0]; + IsOverridable = true; + Value = initialValue; + Dirty = true; // We start dirty by default, set to false if this isn't true + } + + /// + /// Copy constructor. + /// + /// The VariableLengthField to copy from. + public VariableLengthField(VariableLengthField other) + { + _validBitDepths = new List(other.ValidBitDepths); + _bitDepth = other._bitDepth; + IsOverridable = other.IsOverridable; + _value = other._value; + Dirty = other.Dirty; + } + + /// + /// Private backing store for public ValidBitDepths property. + /// + private List _validBitDepths; + + /// + /// This property reports the valid bit depths for this field. + /// + public ReadOnlyCollection ValidBitDepths + { + get + { + return _validBitDepths.AsReadOnly(); + } + } + + /// + /// Backing store for BitDepth. + /// + private int _bitDepth; + + /// + /// This property gets or sets the BitDepth for this field. It is the responsibility + /// of the caller to ensure that BitDepth is not set too low to house the current Value property. + /// Any attempt to do this will throw an exception. + /// + public int BitDepth + { + get + { + return _bitDepth; + } + set + { + if (false == _validBitDepths.Contains(value)) + { + throw new ArgumentException( + String.Format(CultureInfo.InvariantCulture, "Cannot set BitDepth to {0}, this is not a valid value", + value), nameof(value)); + } + + int log2 = Log2(Value); + if (log2 + 1 > value) + { + throw new ArgumentException( + String.Format(CultureInfo.InvariantCulture, "Cannot set BitDepth to {0}, current Value of {1} would be lost!", + value, Value), nameof(value)); + } + + _bitDepth = value; + } + } + + /// + /// This property gets or sets whether BitDepth is overridable, or free to change automatically. + /// If true, then BitDepth may be adjusted when Value is set. If false, then BitDepth can only + /// be changed explicitly by having the caller set it. + /// + public bool IsOverridable { get; set; } + + /// + /// Backing store for Value. + /// + private UInt64 _value; + + /// + /// Gets and sets the value of this field. If IsOverridable is false (default), the setting of + /// Value will cause BitDepth to automatically be set to the minimum value which can house Value. + /// If IsOverridable is true, then BitDepth will not change. It is the responsiblity of the caller + /// to ensure that Value will not be set beyond what can be represented by the maximum, or the current + /// non-overridable BitDepth. Any attempt to do so will throw an exception. + /// + public UInt64 Value + { + get + { + return _value; + } + + set + { + // Resize the bit depth to just fit the requested value + int log2 = Log2(value); + + // Will this fit in the biggest valid bit depth? + if (log2 + 1 > _validBitDepths.Last()) + { + throw new ArgumentOutOfRangeException(nameof(value), + String.Format(CultureInfo.InvariantCulture, "Cannot set Value to {0}, exceeds maximum bit depth of {1}!", + value, _validBitDepths.Last())); + } + + int newBitDepth = _validBitDepths.First(bitDepth => bitDepth >= log2 + 1); + + // Are we allowed to override? + if (IsOverridable) + { + // BitDepth (public property) now checks if caller is trying to set to something which + // will not hold the current Value. We haven't even set the new Value yet. So bypass + // and set the private variable directly. + _bitDepth = newBitDepth; + } + else if (log2 + 1 > BitDepth) + { + throw new ArgumentOutOfRangeException(nameof(value), + String.Format(CultureInfo.InvariantCulture, "Cannot set Value to {0}, exceeds non-overridable bit depth of {1}!", + value, BitDepth)); + } + + // Confirm that no bits lie outside the bit depth + Debug.Assert(64 == BitDepth || 0 == (~(((UInt64)1 << BitDepth) - 1) & value)); + + _value = value; + Dirty = true; + } + } + + /// + /// Sets this field to its minimal BitDepth. The typical scenario is that BitDepth was set to match + /// the on-disk bit depth, during deserialization, but it is found that the on-disk bit depth was + /// unnecessarily large (this sometimes cannot be avoided, such as in live scenarios). + /// + /// If IsOverridable is false, then this method does nothing. + /// + public void Compact() + { + if (false == IsOverridable) + return; // Just return silently + + Value = _value; + } + + /// + /// True if this field matches the on-disk representation, either because we deserialized and + /// no further changes were made, or because we just saved to disk and no further changes were made. + /// + public bool Dirty { get; set; } + + /// + /// Helper function to compute the log2 value of an integer. + /// + /// The value to compute log2 of. + /// Log2 of value. + private static int Log2(UInt64 value) + { + if (0 == value) + { + return int.MinValue; + } + + int log2 = 0; + while (value != 0) + { + value >>= 1; + log2 += 1; + } + + return log2 - 1; + } + + + #region Equality Methods + + //===================================================================== + // Equality Methods + // + // In order to implement IEquatable the way in which MSDN recommends, + // it is unfortunately necessary to cut-and-paste this group of functions + // into each derived class and to do a search-and-replace on the types + // to match the derived class type. Generic classes do not work because + // of the special rules around operator overloading and such. + // + // In addition to this cut-and-paste, search-and-replace, the derived + // class should also override the Equals(base) method so that programs + // which attempt to test equality using pointers to base classes will + // be seeing results from the fully derived equality implementations. + // + // In other words, Box box = new DerivedBox(), box.Equals(box2). If + // Equals(base) is not overriden, then the base implementation of + // Equals(base) is used, which will not compare derived fields. + //===================================================================== + + /// + /// Object.Equals override. + /// + /// The object to test equality against. + /// True if the this and the given object are equal. + public override bool Equals(Object? obj) + { + return this.Equals(obj as VariableLengthField); + } + + /// + /// Implements IEquatable(VariableLengthField). This function is virtual and it is expected that + /// derived classes will override it, so that programs which attempt to test equality + /// using pointers to base classes will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public virtual bool Equals(VariableLengthField? other) + { + // If parameter is null, return false. + if (Object.ReferenceEquals(other, null)) + { + return false; + } + + // Optimization for a common success case. + if (Object.ReferenceEquals(this, other)) + { + return true; + } + + // If run-time types are not exactly the same, return false. + if (this.GetType() != other.GetType()) + return false; + + // Compare fields. We only care if the values match. We don't care about BitDepth or IsOverridable. + if (Value != other.Value) + return false; + + // If we reach this point, the fields all match + return true; + } + + /// + /// Object.GetHashCode override. This must be done as a consequence of overridding + /// Object.Equals. + /// + /// Hash code which will be match the hash code of an object which is equal. + public override int GetHashCode() + { + return Value.GetHashCode(); + } + + /// + /// Override == operation (as recommended by MSDN). + /// + /// The box on the left-hand side of the ==. + /// The box on the right-hand side of the ==. + /// True if the two boxes are equal. + public static bool operator ==(VariableLengthField? lhs, VariableLengthField? rhs) + { + // Check for null on left side. + if (Object.ReferenceEquals(lhs, null)) + { + if (Object.ReferenceEquals(rhs, null)) + { + // null == null = true. + return true; + } + + // Only the left side is null. + return false; + } + // Equals handles case of null on right side. + return lhs.Equals(rhs); + } + + /// + /// Override != operation (as recommended by MSDN). + /// + /// The box on the left-hand side of the !=. + /// The box on the right-hand side of the !=. + /// True if the two boxes are equal. + public static bool operator !=(VariableLengthField? lhs, VariableLengthField? rhs) + { + return !(lhs == rhs); + } + + #endregion + + } +} diff --git a/fmp4/hdlrBox.cs b/fmp4/hdlrBox.cs new file mode 100644 index 0000000..24ffd83 --- /dev/null +++ b/fmp4/hdlrBox.cs @@ -0,0 +1,373 @@ + +using System.Globalization; +using System.Diagnostics; +using System.Text; + +namespace AMSMigrate.Fmp4 +{ + /// + /// Represents a Handler Reference Box ('hdlr'). ISO 14496-12 Sec:8.4.3 + /// + public class hdlrBox : FullBox, IEquatable + { + /// + /// Default constructor. + /// + public hdlrBox() : + base(version: 0, flags: 0, boxtype: MP4BoxType.hdlr) + { + Size.Value = ComputeSize(); // Derived class is responsible for updating Size.Value after construction complete + } + + /// + /// Copy constructor + /// + /// the box to copy from + public hdlrBox(Box box) : + base(box) + { + Debug.Assert(box.Type == MP4BoxType.hdlr); + } + + /// + /// parse the body of box. + /// + protected override void ReadBody() + { + long startOfFullBox = Body!.BaseStream.Position; + base.ReadBody(); + long startPosition = Body.BaseStream.Position; + try + { + UInt32 pre_defined = Body.ReadUInt32(); + if (0 != pre_defined) + { + SetDirty(); + } + + _handlerType = Body.ReadUInt32(); + + // Read past the three reserved ints + for (int i = 0; i < 3; i++) + { + UInt32 reserved = Body.ReadUInt32(); + if (0 != reserved) + { + SetDirty(); + } + } + + // Read the remaining bytes into a byte buffer and convert from UTF-8 to string + long endOfBox = startOfFullBox + (long)Size.Value - BodyPreBytes; + int cbBytesToRead = (int)(endOfBox - Body.BaseStream.Position); + if (cbBytesToRead > 0) + { + byte[] strBytes = Body.ReadBytes(cbBytesToRead); + + // Check if last character was a null-terminator. If so, just trim that one. + // It is entirely possible for multiple null-terms to exist in name, we won't touch. + int cchString; + if ('\0' == strBytes[strBytes.Length - 1]) + { + cchString = strBytes.Length - 1; + } + else + { + cchString = strBytes.Length; + } + _name = Encoding.UTF8.GetString(strBytes, 0, cchString); + + // Check for roundtrip. Might not round-trip if, for instance, early or multiple null-terminator, + // or lack of null-terminator. + int cbBytesToWrite = Encoding.UTF8.GetByteCount(_name) + 1; // We will always output *one* null-terminator + if (cbBytesToRead != cbBytesToWrite) + { + SetDirty(); + } + } + else + { + Debug.Assert(null == Name); + } + } + catch (EndOfStreamException ex) + { + // Reported error offset will point to start of box + throw new MP4DeserializeException(TypeDescription, -BodyPreBytes, BodyInitialOffset, + String.Format(CultureInfo.InvariantCulture, "Could not read hdlr fields, only {0} bytes left in reported size, expected {1}", + Body.BaseStream.Length - startPosition, ComputeLocalSize()), ex); + } + } + + /// + /// This function does the actual work of serializing this class to disk. + /// + /// The MP4Writer to write to. + protected override void WriteToInternal(MP4Writer writer) + { + UInt64 thisSize = ComputeLocalSize(); + + // Write out the FullBox first + base.WriteToInternal(writer); + + long startPosition = writer.BaseStream.Position; + + writer.WriteUInt32(0); // pre_defined + writer.WriteUInt32(_handlerType); // handler_type + for (int i = 0; i < 3; i++) + { + writer.WriteUInt32(0); // reserved + } + + // If Name is null, we won't even write out the null terminator + if (null != Name) + { + byte[] strBytes = Encoding.UTF8.GetBytes(Name); + writer.Write(strBytes); // name + writer.Write((byte)0); // null-terminator + } + + // Confirm that we wrote exactly the number of bytes we said we would + Debug.Assert(writer.BaseStream.Position - startPosition == (long)thisSize || + Stream.Null == writer.BaseStream); + } + + /// + /// Backing store for public HandlerType property. + /// + private UInt32 _handlerType; + + /// + /// The handler_type field of this 'hdlr' box. + /// + public UInt32 HandlerType + { + get + { + return _handlerType; + } + set + { + _handlerType = value; + SetDirty(); + } + } + + /// + /// Backing store for the public Name property. + /// + private string? _name; + + /// + /// The name field of this 'hdlr' box. + /// + public string? Name + { + get + { + return _name; + } + set + { + _name = value; + SetDirty(); + } + } + + /// + /// A list of known values for HandlerType. + /// + public static class HandlerTypes + { + // The following definitions follow spec casing, and thus generate CA1709 - suppress in GlobalSuppressions.cs to retain readability + public const UInt32 vide = 0x76696465; // 'vide', Video track + public const UInt32 soun = 0x736F756E; // 'soun', Audio track + public const UInt32 hint = 0x68696E74; // 'hint', Hint track + public const UInt32 meta = 0x6D657461; // 'meta', Timed Metadata track + public const UInt32 auxv = 0x61757876; // 'auxv', Auxiliary Video track + } + + /// + /// Calculate the current size of this box. + /// + /// The current size of this box, if it were to be written to disk now. + public override UInt64 ComputeSize() + { + UInt64 thisSize = ComputeLocalSize(); + UInt64 baseSize = base.ComputeSize(); + + return thisSize + baseSize; + } + + /// + /// Calculates the current size of just this class (base classes excluded). This is called by + /// ComputeSize(). + /// + /// The current size of just the fields from this box. + protected new UInt64 ComputeLocalSize() + { + UInt64 cbName; + if (null == Name) + { + cbName = 0; + } + else + { + cbName = (UInt64)Encoding.UTF8.GetByteCount(Name) + 1; // Add null-term + } + + return 4 + // pre_defined + 4 + // handler_type + 12 + // reserved + cbName; // name plus null-term + } + + + #region Equality Methods + + //===================================================================== + // Equality Methods + // + // In order to implement IEquatable the way in which MSDN recommends, + // it is unfortunately necessary to cut-and-paste this group of functions + // into each derived class and to do a search-and-replace on the types + // to match the derived class type. Generic classes do not work because + // of the special rules around operator overloading and such. + // + // In addition to this cut-and-paste, search-and-replace, the derived + // class should also override the Equals(base) method so that programs + // which attempt to test equality using pointers to base classes will + // be seeing results from the fully derived equality implementations. + // + // In other words, Box box = new DerivedBox(), box.Equals(box2). If + // Equals(base) is not overriden, then the base implementation of + // Equals(base) is used, which will not compare derived fields. + //===================================================================== + + /// + /// Object.Equals override. + /// + /// The object to test equality against. + /// True if the this and the given object are equal. + public override bool Equals(Object? obj) + { + return this.Equals(obj as hdlrBox); + } + + /// + /// Box.Equals override. This is done so that programs which attempt to test equality + /// using pointers to Box will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public override bool Equals(Box? other) + { + return this.Equals(other as hdlrBox); + } + + /// + /// FullBox.Equals override. This is done so that programs which attempt to test equality + /// using pointers to Box will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public override bool Equals(FullBox? other) + { + return this.Equals(other as hdlrBox); + } + + /// + /// Implements IEquatable(hdlrBox). This function is virtual and it is expected that + /// derived classes will override it, so that programs which attempt to test equality + /// using pointers to base classes will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public virtual bool Equals(hdlrBox? other) + { + // If parameter is null, return false. + if (Object.ReferenceEquals(other, null)) + { + return false; + } + + // Optimization for a common success case. + if (Object.ReferenceEquals(this, other)) + { + return true; + } + + // If run-time types are not exactly the same, return false. + if (this.GetType() != other.GetType()) + return false; + + // === First, check if base classes are equal === + + if (false == base.Equals(other)) + return false; + + // === Now, compare the fields which are specific to this class === + + if (HandlerType != other.HandlerType) + return false; + + if (Name != other.Name) + return false; + + // If we reach this point, the fields all match + return true; + } + + /// + /// Object.GetHashCode override. This must be done as a consequence of overridding + /// Object.Equals. + /// + /// Hash code which will be match the hash code of an object which is equal. + public override int GetHashCode() + { + return Type.GetHashCode(); + } + + /// + /// Override == operation (as recommended by MSDN). + /// + /// The box on the left-hand side of the ==. + /// The box on the right-hand side of the ==. + /// True if the two boxes are equal. + public static bool operator ==(hdlrBox? lhs, hdlrBox? rhs) + { + // Check for null on left side. + if (Object.ReferenceEquals(lhs, null)) + { + if (Object.ReferenceEquals(rhs, null)) + { + // null == null = true. + return true; + } + + // Only the left side is null. + return false; + } + // Equals handles case of null on right side. + return lhs.Equals(rhs); + } + + /// + /// Override != operation (as recommended by MSDN). + /// + /// The box on the left-hand side of the !=. + /// The box on the right-hand side of the !=. + /// True if the two boxes are equal. + public static bool operator !=(hdlrBox? lhs, hdlrBox? rhs) + { + return !(lhs == rhs); + } + + #endregion + + + } +} diff --git a/fmp4/mdatBox.cs b/fmp4/mdatBox.cs new file mode 100644 index 0000000..ded74fb --- /dev/null +++ b/fmp4/mdatBox.cs @@ -0,0 +1,376 @@ + +using System.Diagnostics; +using System.Security.Cryptography; + +namespace AMSMigrate.Fmp4 +{ + /// + /// An object for the 'moof' box from ISO 14496-12. + /// + + public class mdatBox : Box, IEquatable + { + /// + /// Default constructor for the box. + /// + public mdatBox() : + base(MP4BoxType.mdat) + { + // We used to call ComputeSize(), but that is overridable and can cause us to call an uninitialized derived object, so unroll + // THIS class's ComputeSize() here. A derived class will like call its ComputeSize() and overwrite our value. + Size.Value = base.ComputeSize() + ComputeLocalSize(); + } + + /// + /// Deserializing constructor. + /// + /// the box to construct from + public mdatBox(Box box) : + base(box) + { + Debug.Assert(box.Type == MP4BoxType.mdat); + } + + /// + /// Deep copy constructor. + /// + /// The mdat box to deep-copy. + public mdatBox(mdatBox other) : + base(MP4BoxType.mdat) + { + if (null != other.SampleData) + { + SampleData = (byte[])other.SampleData.Clone(); // Clone is shallow copy wrt object refs but with byte it's deep + } + + // Normally would set _prevSampleDataChecksum here, but there is no "correct" value + // since our policy is for a deep copy's Dirty to be true (unconditionally), and today + // this is acheieved via base.Dirty being true. So, save some CPU cycles and don't set it. + // If our copy.Dirty policy changes in future, then: + // copy.Dirty = other.Dirty suggests _prevSampleDataChecksum = other._prevSampleDataChecksum; + // copy.Dirty = false suggests _prevSampleDataChecksum = ComputeSampleDataChecksum(SampleData); + } + + /// + /// Byte array containing the sample data. + /// + public byte[]? SampleData { get; set; } + + /// + /// Checksum of the contents of SampleData the last time Dirty was reset to false. + /// + private byte[]? _prevSampleDataChecksum; + + /// + /// Parses the body of the box. + /// + protected override void ReadBody() + { + base.ReadBody(); + + // We know that Box.cs saves the bytes to a MemoryStream. I tried calling + // MemoryStream.GetBuffer, but got UnauthorizedAccessException. So we can't + // avoid the memory copy. + long startPosition = Body!.BaseStream.Position; + int bytesToRead = (int)(Body.BaseStream.Length - startPosition); + if (bytesToRead > 0) + { + SampleData = Body.ReadBytes(bytesToRead); + } + else + { + SampleData = null; + } + _prevSampleDataChecksum = ComputeSampleDataChecksum(SampleData); + } + + /// + /// Serialize the box contents to the writer. + /// + /// the MP4Writer to write to + protected override void WriteToInternal(MP4Writer writer) + { + base.WriteToInternal(writer); + if (SampleData != null) + { + writer.Write(SampleData); + } + } + + /// + /// Computes the overall size of the box. + /// + /// + public override ulong ComputeSize() + { + return base.ComputeSize() + ComputeLocalSize(); + } + + /// + /// Compute the size of box specific members. + /// + /// + protected new UInt64 ComputeLocalSize() + { + if (null == SampleData) + { + return 0; + } + else + { + return (UInt64)SampleData.Length; + } + } + + /// + /// Checks if the given sample data is null or empty. + /// + /// The sample data array to check. + /// True if null or empty (count of 0). + internal static bool SampleDataIsNullOrEmpty(byte[]? sampleData) + { + if (null == sampleData) + { + return true; + } + + if (0 == sampleData.Length) + { + return true; + } + + return false; + } + + /// + /// Checksum the contents of the given sample data, or if null, return null. + /// + /// The sample data array to checksum. + /// A byte array containing the checksum of the contents of sampleData, + /// or null if sampleData was null. + internal static byte[]? ComputeSampleDataChecksum(byte[]? sampleData) + { + if (SampleDataIsNullOrEmpty(sampleData)) + { + return null; + } + + using (var hash = SHA256.Create()) + { + return hash.ComputeHash(sampleData!); + } + } + + /// + /// Compares a given sample data buffer against a another sample data checksum. + /// + /// The sample data buffer to compare. + /// The checksum of another sample data + /// buffer to compare to. + /// True if they are equal, otherwise false. + internal static bool SampleDataAreEqual(byte[]? thisSampleData, byte[]? thatSampleDataChecksum) + { + var currentChecksum = ComputeSampleDataChecksum(thisSampleData); + var thatChecksum = ComputeSampleDataChecksum(thatSampleDataChecksum); + + if (currentChecksum != thatChecksum) + { + return false; + } + + if (null != currentChecksum && false == currentChecksum.SequenceEqual(thatSampleDataChecksum!)) + { + return false; + } + + // If we reach this point, they are equal + return true; + } + + /// + /// This property indicates whether any of the members of this class are dirty, but not + /// this class's base class. The reason this is separated from base class is explained in + /// Box.cs and has to do with decoupling the dependency chain which arises when base classes + /// call derived classes for information during their construction. + /// + /// It is true if the members of this box match their on-disk representation, either because + /// we deserialized and no further changes were made, or because we just saved to disk + /// and no further changes were made. + /// + public override bool Dirty + { + get + { + if (base.Dirty) + { + return true; + } + + if (false == SampleDataAreEqual(SampleData, _prevSampleDataChecksum)) + { + return true; + } + + // If we reached this stage, we are not dirty + return false; + } + + set + { + base.Dirty = value; + if (false == value) + { + _prevSampleDataChecksum = ComputeSampleDataChecksum(SampleData); + } + } + } + + #region Equality Methods + + //===================================================================== + // Equality Methods + // + // In order to implement IEquatable the way in which MSDN recommends, + // it is unfortunately necessary to cut-and-paste this group of functions + // into each derived class and to do a search-and-replace on the types + // to match the derived class type. Generic classes do not work because + // of the special rules around operator overloading and such. + // + // In addition to this cut-and-paste, search-and-replace, the derived + // class should also override the Equals(base) method so that programs + // which attempt to test equality using pointers to base classes will + // be seeing results from the fully derived equality implementations. + // + // In other words, Box box = new DerivedBox(), box.Equals(box2). If + // Equals(base) is not overriden, then the base implementation of + // Equals(base) is used, which will not compare derived fields. + //===================================================================== + + /// + /// Object.Equals override. + /// + /// The object to test equality against. + /// True if the this and the given object are equal. + public override bool Equals(Object? obj) + { + return this.Equals(obj as mdatBox); + } + + /// + /// Box.Equals override. This is done so that programs which attempt to test equality + /// using pointers to Box will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public override bool Equals(Box? other) + { + return this.Equals(other as mdatBox); + } + + /// + /// Implements IEquatable(mdatBox). This function is virtual and it is expected that + /// derived classes will override it, so that programs which attempt to test equality + /// using pointers to base classes will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public virtual bool Equals(mdatBox? other) + { + // If parameter is null, return false. + if (Object.ReferenceEquals(other, null)) + { + return false; + } + + // Optimization for a common success case. + if (Object.ReferenceEquals(this, other)) + { + return true; + } + + // If run-time types are not exactly the same, return false. + if (this.GetType() != other.GetType()) + return false; + + // === First, check if base classes are equal === + if (false == base.Equals(other)) + return false; + + // === Now, compare the fields which are specific to this class === + bool thisSampleDataNullOrEmpty = SampleDataIsNullOrEmpty(SampleData); + if (SampleDataIsNullOrEmpty(other.SampleData) != thisSampleDataNullOrEmpty) + { + return false; + } + + if (false == thisSampleDataNullOrEmpty && false == Enumerable.SequenceEqual(SampleData!, other.SampleData!)) + { + return false; + } + + // If we reach this point, the fields all match + return true; + } + + /// + /// Object.GetHashCode override. This must be done as a consequence of overridding + /// Object.Equals. + /// + /// Hash code which will be match the hash code of an object which is equal. + public override int GetHashCode() + { + var checksum = ComputeSampleDataChecksum(SampleData); + if (null == checksum) + return 0; + + // Return the lower 32 bits of the checksum XOR hash of any children. + // If checksum length < 4, we expect an exception to be thrown. + int hashCode = BitConverter.ToInt32(checksum, 0); + int childHash = 0; + foreach (Box child in Children) + { + childHash ^= child.GetHashCode(); + } + return (hashCode ^ childHash); + } + + /// + /// Override == operation (as recommended by MSDN). + /// + /// The box on the left-hand side of the ==. + /// The box on the right-hand side of the ==. + /// True if the two boxes are equal. + public static bool operator ==(mdatBox? lhs, mdatBox? rhs) + { + // Check for null on left side. + if (Object.ReferenceEquals(lhs, null)) + { + if (Object.ReferenceEquals(rhs, null)) + { + // null == null = true. + return true; + } + + // Only the left side is null. + return false; + } + // Equals handles case of null on right side. + return lhs.Equals(rhs); + } + + /// + /// Override != operation (as recommended by MSDN). + /// + /// The box on the left-hand side of the !=. + /// The box on the right-hand side of the !=. + /// True if the two boxes are equal. + public static bool operator !=(mdatBox? lhs, mdatBox? rhs) + { + return !(lhs == rhs); + } + + #endregion + } +} diff --git a/fmp4/mdhdBox.cs b/fmp4/mdhdBox.cs new file mode 100644 index 0000000..6a15041 --- /dev/null +++ b/fmp4/mdhdBox.cs @@ -0,0 +1,498 @@ + +using System.Diagnostics; +using System.Globalization; + +namespace AMSMigrate.Fmp4 +{ + /// + /// Represents a Media Header Box ('mdhd'). ISO 14496-12 Sec:8.4.2.1. + /// + public class mdhdBox : FullBox, IEquatable + { + /// + /// Default constructor. + /// + public mdhdBox() : + base(version: 1, flags: 0, boxtype: MP4BoxType.mdhd) + { + Size.Value = ComputeSize(); // Derived class is responsible for updating Size.Value after construction complete + } + + /// + /// Copy constructor + /// + /// the box to copy from + public mdhdBox(Box box) : + base(box) + { + Debug.Assert(box.Type == MP4BoxType.mdhd); + } + + /// + /// parse the body of box. + /// + protected override void ReadBody() + { + base.ReadBody(); + + long startPosition = Body!.BaseStream.Position; + + // Check on version - we expect 0 or 1, reject anything else + if (Version > 1) + { + // Reported error offset will point to Version byte in FullBox + throw new MP4DeserializeException(TypeDescription, 0, BodyInitialOffset, + String.Format(CultureInfo.InvariantCulture, "Unexpected version: {0}. Expected 0 or 1!", Version)); + } + + try + { + if (1 == Version) + { + _creationTime = Body.ReadUInt64(); + _modificationTime = Body.ReadUInt64(); + _timeScale = Body.ReadUInt32(); + _duration = Body.ReadUInt64(); + } + else + { + Debug.Assert(0 == Version); + _creationTime = Body.ReadUInt32(); + _modificationTime = Body.ReadUInt32(); + _timeScale = Body.ReadUInt32(); + _duration = Body.ReadUInt32(); + SetDirty(); // We always write Version = 1 for now, TFS #713203 + } + + // If we wanted to, we could check for _timeScale == 0, but I am choosing not + // to do so at this time just in case it doesn't end up getting used. + + // ISO-639-2/T language code, packed as difference between ASCII value and 0x60, + // resulting in three lower-case letters. + UInt16 languageInt = Body.ReadUInt16(); + if (0 != (languageInt & 0x8000)) + { + SetDirty(); // The current spec says pad == "0", so we cannot round-trip this - we're dirty. + } + + var languageChars = new char[3]; + for (int i = languageChars.Length - 1; i >= 0; i--) + { + char languageChar = (char)((languageInt & 0x1F) + 0x60); + languageChars[i] = languageChar; + + // Advance variables + languageInt >>= 5; + } + _language = new string(languageChars); + + UInt16 pre_defined = Body.ReadUInt16(); + if (0 != pre_defined) + { + SetDirty(); // The current spec says pre_defined == "0", so we cannot round-trip this - we're dirty. + } + } + catch (EndOfStreamException ex) + { + // Reported error offset will point to start of box + throw new MP4DeserializeException(TypeDescription, -BodyPreBytes, BodyInitialOffset, + String.Format(CultureInfo.InvariantCulture, "Could not read hdlr fields, only {0} bytes left in reported size, expected {1}", + Body.BaseStream.Length - startPosition, ComputeLocalSize()), ex); + } + } + + /// + /// This function does the actual work of serializing this class to disk. + /// + /// The MP4Writer to write to. + protected override void WriteToInternal(MP4Writer writer) + { + Version = 1; // We always write Version = 1 for now, TFS #713203 + + UInt64 thisSize = ComputeLocalSize(); + + // Write out the FullBox first + base.WriteToInternal(writer); + + long startPosition = writer.BaseStream.Position; + + if (1 == Version) + { + writer.WriteUInt64(CreationTime); + writer.WriteUInt64(ModificationTime); + writer.WriteUInt32(TimeScale); + writer.WriteUInt64(Duration); + } + else + { + Debug.Assert(0 == Version); + writer.WriteUInt32((UInt32)CreationTime); + writer.WriteUInt32((UInt32)ModificationTime); + writer.WriteUInt32(TimeScale); + writer.WriteUInt32((UInt32)Duration); + } + + CheckLanguage(Language); + UInt16 languageInt = 0; + if (false == String.IsNullOrEmpty(Language)) + { + for (int i = 0; i < Language.Length; i++) + { + languageInt = (UInt16)((languageInt << 5) | ((Language[i] - 0x60) & 0x1F)); + } + } + writer.WriteUInt16(languageInt); + + writer.WriteUInt16(0); // pre_defined + + // Confirm that we wrote exactly the number of bytes we said we would + Debug.Assert(writer.BaseStream.Position - startPosition == (long)thisSize || + Stream.Null == writer.BaseStream); + } + + /// + /// Calculate the current size of this box. + /// + /// The current size of this box, if it were to be written to disk now. + public override UInt64 ComputeSize() + { + UInt64 thisSize = ComputeLocalSize(); + UInt64 baseSize = base.ComputeSize(); + + return thisSize + baseSize; + } + + /// + /// Calculates the current size of just this class (base classes excluded). This is called by + /// ComputeSize(). + /// + /// The current size of just the fields from this box. + protected static new UInt64 ComputeLocalSize() + { + UInt64 commonSize = 4 + // timescale + 2 + // pad + language + 2; // pre_defined + + // We always write Version = 1 for now, TFS #713203 + UInt64 sizeByVersion = 3 * 8; // creation_time, modification_time, duration @ 64-bit + + return commonSize + sizeByVersion; + } + + /// + /// Backing store for public CreationTime property. + /// + private UInt64 _creationTime; + + /// + /// An integer that declares the creation time of the media in this track, in seconds since + /// midnight, Jan 1, 1904 UTC (ISO 14496-12:2012 Sec:8.4.2.3). + /// + public UInt64 CreationTime + { + get + { + return _creationTime; + } + + set + { + _creationTime = value; + SetDirty(); + } + } + + /// + /// Backing store for public ModificationTime property. + /// + private UInt64 _modificationTime; + + /// + /// An integer that declares the most recent time the media in this track was modified, + /// in seconds since midnight, Jan 1, 1904 UTC (ISO 14496-12:2012 Sec:8.4.2.3). + /// + public UInt64 ModificationTime + { + get + { + return _modificationTime; + } + + set + { + _modificationTime = value; + SetDirty(); + } + } + + /// + /// Backing store for public TimeScale property. + /// + private UInt32 _timeScale = 1; + + /// + /// An integer that specifies the time-scale for this media; this is the number of time units + /// that pass in one second. For example, a time coordinate system that measures time in + /// sixtieths of a second has a time scale of 60. (ISO 14496-12:2012 Sec:8.4.2.3). + /// + public UInt32 TimeScale + { + get + { + return _timeScale; + } + + set + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value), + String.Format(CultureInfo.InvariantCulture, "Mdhd.TimeScale must be greater than 0: {0}!", value)); + } + + _timeScale = value; + SetDirty(); + } + } + + /// + /// Backing store for public Duration property. + /// + private UInt64 _duration; + + /// + /// An integer that declares the duration of this media (in the scale of the timescale). + /// If the duration cannot be determined then duration is set to all 1s. + /// (ISO 14496-12:2012 Sec:8.4.2.3). + /// + public UInt64 Duration + { + get + { + return _duration; + } + + set + { + _duration = value; + SetDirty(); + } + } + + /// + /// Backing store for public Language property. + /// + private string _language = "und"; + + /// + /// Declares the language code for this media. See ISO 639-2/T for the set of three + /// character codes. The result should be three lower-case letters. + /// (ISO 14496-12:2012 Sec:8.4.2.3). This cannot be set to null or \0\0\0 and so + /// the default value shall be 'und'. + /// + public string Language + { + get + { + return _language; + } + + set + { + CheckLanguage(value); + _language = value; + SetDirty(); + } + } + + private static void CheckLanguage(string language) + { + if (null == language) + { + throw new ArgumentException("Language cannot be set to null! Use 'und' instead.", + nameof(language)); + } + + if (3 != language.Length) + { + throw new ArgumentException( + String.Format(CultureInfo.InvariantCulture, "Language must be null or else 3 characters exactly: {0}!", language), + nameof(language)); + } + + for (int i = 0; i < language.Length; i++) + { + char languageChar = language[i]; + if (languageChar < 0x60) + { + throw new ArgumentException( + String.Format(CultureInfo.InvariantCulture, "Language {0} has a character value less than 0x60 (0x{1:X}) at position {2}!", + language, (int)languageChar, i), nameof(language)); + } + + if (0 != ((languageChar - 0x60) & ~0x1F)) + { + throw new ArgumentException( + String.Format(CultureInfo.InvariantCulture, "Language {0} has a character value which cannot be represented in 5 bits (0x{1:X}) at position {2}!", + language, (int)languageChar, i), nameof(language)); + } + } + } + + + #region Equality Methods + + //===================================================================== + // Equality Methods + // + // In order to implement IEquatable the way in which MSDN recommends, + // it is unfortunately necessary to cut-and-paste this group of functions + // into each derived class and to do a search-and-replace on the types + // to match the derived class type. Generic classes do not work because + // of the special rules around operator overloading and such. + // + // In addition to this cut-and-paste, search-and-replace, the derived + // class should also override the Equals(base) method so that programs + // which attempt to test equality using pointers to base classes will + // be seeing results from the fully derived equality implementations. + // + // In other words, Box box = new DerivedBox(), box.Equals(box2). If + // Equals(base) is not overriden, then the base implementation of + // Equals(base) is used, which will not compare derived fields. + //===================================================================== + + /// + /// Object.Equals override. + /// + /// The object to test equality against. + /// True if the this and the given object are equal. + public override bool Equals(Object? obj) + { + return this.Equals(obj as mdhdBox); + } + + /// + /// Box.Equals override. This is done so that programs which attempt to test equality + /// using pointers to Box will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public override bool Equals(Box? other) + { + return this.Equals(other as mdhdBox); + } + + /// + /// FullBox.Equals override. This is done so that programs which attempt to test equality + /// using pointers to Box will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public override bool Equals(FullBox? other) + { + return this.Equals(other as mdhdBox); + } + + /// + /// Implements IEquatable(mdhdBox). This function is virtual and it is expected that + /// derived classes will override it, so that programs which attempt to test equality + /// using pointers to base classes will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public virtual bool Equals(mdhdBox? other) + { + // If parameter is null, return false. + if (Object.ReferenceEquals(other, null)) + { + return false; + } + + // Optimization for a common success case. + if (Object.ReferenceEquals(this, other)) + { + return true; + } + + // If run-time types are not exactly the same, return false. + if (this.GetType() != other.GetType()) + return false; + + // === First, check if base classes are equal === + + if (false == base.Equals(other)) + return false; + + // === Now, compare the fields which are specific to this class === + + if (CreationTime != other.CreationTime) + return false; + + if (ModificationTime != other.ModificationTime) + return false; + + if (TimeScale != other.TimeScale) + return false; + + if (Duration != other.Duration) + return false; + + if (Language != other.Language) + return false; + + // If we reach this point, the fields all match + return true; + } + + /// + /// Object.GetHashCode override. This must be done as a consequence of overridding + /// Object.Equals. + /// + /// Hash code which will be match the hash code of an object which is equal. + public override int GetHashCode() + { + return base.GetHashCode(); // All fields of mdhd are mutable + } + + /// + /// Override == operation (as recommended by MSDN). + /// + /// The box on the left-hand side of the ==. + /// The box on the right-hand side of the ==. + /// True if the two boxes are equal. + public static bool operator ==(mdhdBox? lhs, mdhdBox? rhs) + { + // Check for null on left side. + if (Object.ReferenceEquals(lhs, null)) + { + if (Object.ReferenceEquals(rhs, null)) + { + // null == null = true. + return true; + } + + // Only the left side is null. + return false; + } + // Equals handles case of null on right side. + return lhs.Equals(rhs); + } + + /// + /// Override != operation (as recommended by MSDN). + /// + /// The box on the left-hand side of the !=. + /// The box on the right-hand side of the !=. + /// True if the two boxes are equal. + public static bool operator !=(mdhdBox? lhs, mdhdBox? rhs) + { + return !(lhs == rhs); + } + + #endregion + + } +} diff --git a/fmp4/mdiaBox.cs b/fmp4/mdiaBox.cs new file mode 100644 index 0000000..7f1776a --- /dev/null +++ b/fmp4/mdiaBox.cs @@ -0,0 +1,192 @@ + +using System.Diagnostics; + +namespace AMSMigrate.Fmp4 +{ + /// + /// Represents a Media Box ('mdia'). ISO 14496-12 Sec:8.4.1 + /// + public class mdiaBox : Box, IEquatable + { + /// + /// Default constructor. + /// + public mdiaBox() : + base(MP4BoxType.mdia) + { + Size.Value = ComputeSize(); // Derived class is responsible for updating Size.Value after construction complete + } + + /// + /// Copy constructor + /// + /// the box to copy from + public mdiaBox(Box box) : + base(box) + { + Debug.Assert(box.Type == MP4BoxType.mdia); + } + + /// + /// parse the body of box. + /// + protected override void ReadBody() + { + //the body of an mdia box is just its children. + ReadChildren(); + } + + /// + /// Returns the Handler Reference Box ('hdlr') of this 'mdia' (mandatory). + /// + public hdlrBox? Handler + { + get + { + var hdlr = GetExactlyOneChildBox(); + return hdlr; + } + } + + /// + /// Returns the Media Header Box ('mdhd') of this 'mdia' (mandatory). + /// + public mdhdBox? MediaHeader + { + get + { + var mdhd = GetExactlyOneChildBox(); + return mdhd; + } + } + + #region Equality Methods + + //===================================================================== + // Equality Methods + // + // In order to implement IEquatable the way in which MSDN recommends, + // it is unfortunately necessary to cut-and-paste this group of functions + // into each derived class and to do a search-and-replace on the types + // to match the derived class type. Generic classes do not work because + // of the special rules around operator overloading and such. + // + // In addition to this cut-and-paste, search-and-replace, the derived + // class should also override the Equals(base) method so that programs + // which attempt to test equality using pointers to base classes will + // be seeing results from the fully derived equality implementations. + // + // In other words, Box box = new DerivedBox(), box.Equals(box2). If + // Equals(base) is not overriden, then the base implementation of + // Equals(base) is used, which will not compare derived fields. + //===================================================================== + + /// + /// Object.Equals override. + /// + /// The object to test equality against. + /// True if the this and the given object are equal. + public override bool Equals(Object? obj) + { + return this.Equals(obj as mdiaBox); + } + + /// + /// Box.Equals override. This is done so that programs which attempt to test equality + /// using pointers to Box will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public override bool Equals(Box? other) + { + return this.Equals(other as mdiaBox); + } + + /// + /// Implements IEquatable(mdiaBox). This function is virtual and it is expected that + /// derived classes will override it, so that programs which attempt to test equality + /// using pointers to base classes will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public virtual bool Equals(mdiaBox? other) + { + // If parameter is null, return false. + if (Object.ReferenceEquals(other, null)) + { + return false; + } + + // Optimization for a common success case. + if (Object.ReferenceEquals(this, other)) + { + return true; + } + + // If run-time types are not exactly the same, return false. + if (this.GetType() != other.GetType()) + return false; + + // === First, check if base classes are equal === + + if (false == base.Equals(other)) + return false; + + // === Now, compare the fields which are specific to this class === + // Base class already compared all the Children boxes + + // If we reach this point, the fields all match + return true; + } + + /// + /// Object.GetHashCode override. This must be done as a consequence of overridding + /// Object.Equals. + /// + /// Hash code which will be match the hash code of an object which is equal. + public override int GetHashCode() + { + return Type.GetHashCode(); + } + + /// + /// Override == operation (as recommended by MSDN). + /// + /// The box on the left-hand side of the ==. + /// The box on the right-hand side of the ==. + /// True if the two boxes are equal. + public static bool operator ==(mdiaBox? lhs, mdiaBox? rhs) + { + // Check for null on left side. + if (Object.ReferenceEquals(lhs, null)) + { + if (Object.ReferenceEquals(rhs, null)) + { + // null == null = true. + return true; + } + + // Only the left side is null. + return false; + } + // Equals handles case of null on right side. + return lhs.Equals(rhs); + } + + /// + /// Override != operation (as recommended by MSDN). + /// + /// The box on the left-hand side of the !=. + /// The box on the right-hand side of the !=. + /// True if the two boxes are equal. + public static bool operator !=(mdiaBox? lhs, mdiaBox? rhs) + { + return !(lhs == rhs); + } + + #endregion + + } +} diff --git a/fmp4/mfhdBox.cs b/fmp4/mfhdBox.cs new file mode 100644 index 0000000..77e4360 --- /dev/null +++ b/fmp4/mfhdBox.cs @@ -0,0 +1,195 @@ + +using System.Diagnostics; +using System.Globalization; + +namespace AMSMigrate.Fmp4 +{ + /// + /// Represents a movie fragment header box . ISO 14496-12 Sec:8.33.1 + /// + public class mfhdBox : FullBox, IEquatable + { + /// + /// Default consturctor with a sequence number + /// + /// The sequence number of the fragment. + public mfhdBox(UInt32 sequenceNumber) : + base(version:0, flags:0, boxtype:MP4BoxType.mfhd) + { + SequenceNumber = sequenceNumber; + Size.Value = ComputeSize(); + } + + /// + /// Copy constructor + /// + /// the box to copy from + public mfhdBox(Box box): + base(box) + { + Debug.Assert(box.Type == MP4BoxType.mfhd); + } + + /// + /// Get or set the sequence number for this fragment. + /// + public UInt32 SequenceNumber + { + get + { + return _sequenceNumber; + } + set + { + _sequenceNumber = value; + SetDirty(); + } + } + + /// + /// The sequence number for this fragment. + /// + private UInt32 _sequenceNumber; + + /// + /// Parse the body contents of this box. + /// + /// + protected override void ReadBody() + { + base.ReadBody(); + + long startPosition = Body!.BaseStream.Position; + + try + { + _sequenceNumber = Body.ReadUInt32(); + } + catch (EndOfStreamException ex) + { + // Reported error offset will point to start of box + throw new MP4DeserializeException(TypeDescription, -BodyPreBytes, BodyInitialOffset, + String.Format(CultureInfo.InvariantCulture, "Could not read sequence number, only {0} bytes left in reported size, expected {1}", + Body.BaseStream.Length - startPosition, ComputeLocalSize()), ex); + } + + } + + /// + /// Serialize the box contents + /// + /// MP4Writer to write to + protected override void WriteToInternal(MP4Writer writer) + { + base.WriteToInternal(writer); + writer.WriteUInt32(_sequenceNumber); + } + + /// + /// Compute the size of the box. + /// + /// size of the box itself + public override UInt64 ComputeSize() + { + return base.ComputeSize() + ComputeLocalSize(); + } + + /// + /// Compute the size of just the box specific contents. + /// + /// + protected static new UInt64 ComputeLocalSize() + { + return 4; //just the sequence number; + } + + + #region equality methods + + /// + /// Compare this box to another object. + /// + /// the object to compare equality against + /// + public override bool Equals(Object? other) + { + return this.Equals(other as mfhdBox); + } + + /// + /// Returns the hascode of the object. + /// + /// + public override int GetHashCode() + { + return base.GetHashCode(); + } + + /// + /// Box.Equals override. This is done so that programs which attempt to test equality + /// using pointers to Box will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public override bool Equals(Box? other) + { + return this.Equals(other as mfhdBox); + } + + /// + /// FullBox.Equals override. This is done so that programs which attempt to test equality + /// using pointers to Box will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public override bool Equals(FullBox? other) + { + return this.Equals(other as mfhdBox); + } + + /// + /// Compare two mfhdBoxes for equality. + /// + /// other mfhdBox to compare against + /// + public bool Equals(mfhdBox? other) + { + if (!base.Equals((FullBox?)other)) + return false; + + return SequenceNumber == other.SequenceNumber; + } + + /// + /// Compare two mfhdBox objects for equality. + /// + /// left hand side of == + /// right hand side of == + /// + public static bool operator ==(mfhdBox? lhs, mfhdBox? rhs) + { + // Check for null on left side. + if (Object.ReferenceEquals(lhs, null)) + { + if (Object.ReferenceEquals(rhs, null)) + { + // null == null = true. + return true; + } + + // Only the left side is null. + return false; + } + return lhs.Equals(rhs); + } + + public static bool operator !=(mfhdBox? lhs, mfhdBox? rhs) + { + return !(lhs == rhs); + } + + #endregion + } +} diff --git a/fmp4/moofBox.cs b/fmp4/moofBox.cs new file mode 100644 index 0000000..338a7bd --- /dev/null +++ b/fmp4/moofBox.cs @@ -0,0 +1,133 @@ + +using System.Diagnostics; + +namespace AMSMigrate.Fmp4 +{ + /// + /// An object for the 'moof' box from ISO 14496-12. + /// + public class moofBox : Box, IEquatable + { + /// + /// Default constructor for the box. + /// + public moofBox() : + base(MP4BoxType.moof) + { + //at the min have the header box. + Children.Add(new mfhdBox(1)); + + Size.Value = ComputeSize(); + } + + /// + /// Deserializing/copy constructor. + /// + /// the box to construct from + public moofBox(Box box): + base(box) + { + Debug.Assert(box.Type == MP4BoxType.moof); + } + + /// + /// Parses the body of box. A moof is nothing but a collection of child boxes. + /// + protected override void ReadBody() + { + ReadChildren(); + } + + + /// + /// Returns a single child header box for this fragment. + /// + public mfhdBox Header + { + get + { + return Children.Where((box) => box is mfhdBox).Cast().Single(); + } + } + + /// + /// Returns the single Child track fragment box of this fragment. + /// + public trafBox Track + { + get + { + return Children.Where((box) => box is trafBox).Cast().Single(); + } + } + + #region equality methods. + + /// + /// Compare the moofbox against another object for equality. + /// + /// the other object to compare with + /// + public override bool Equals(Object? other) + { + return this.Equals(other as moofBox); + } + + /// + /// returns the hashcode of the object. + /// + /// + public override int GetHashCode() + { + return base.GetHashCode(); + } + + /// + /// Compare two boxes for equality. Use base.Equals() to do the comparsion + /// + /// other box to compare against. + /// true if both boxes are equal else false. + public bool Equals(moofBox? other) + { + return base.Equals(other as Box); + } + + /// + /// Compare two moofBox objects for equality. + /// + /// left side of == + /// right side of == + /// true if the two boxes are equal else false. + public static bool operator ==(moofBox? lhs, moofBox? rhs) + { + // Check for null on left side. + if (Object.ReferenceEquals(lhs, null)) + { + if (Object.ReferenceEquals(rhs, null)) + { + // null == null = true. + return true; + } + + // Only the left side is null. + return false; + } + return lhs.Equals(rhs); + } + + /// + /// compares two entries using the Equals() method for equality. + /// + /// left side of != + /// right side of != + /// return true if two boxes are not equal else false. + public static bool operator !=(moofBox? lhs, moofBox? rhs) + { + return !(lhs == rhs); + } + + + #endregion + + } +} diff --git a/fmp4/moovBox.cs b/fmp4/moovBox.cs new file mode 100644 index 0000000..5c982c3 --- /dev/null +++ b/fmp4/moovBox.cs @@ -0,0 +1,72 @@ + +using System.Diagnostics; + +namespace AMSMigrate.Fmp4 +{ + /// + /// An object model for the moov Box from ISO 14496-12 Sec:8.2.1 + /// + public class moovBox : Box + { + /// + /// Default constructor for the box. + /// + public moovBox() : + base(MP4BoxType.moov) + { + } + + /// + /// Deserializing/copy constructor. + /// + /// the box to construct from + public moovBox(Box box): + base(box) + { + Debug.Assert(box.Type == MP4BoxType.moov); + } + + /// + /// Pases the body of box. A moov is nothing but a collection of child boxes. + /// + protected override void ReadBody() + { + ReadChildren(); + } + + + /// + /// Returns a single child header box for this fragment. + /// + public mvhdBox Header + { + get + { + return Children.Where((box) => box is mvhdBox).Cast().Single(); + } + } + + /// + /// Returns a collection of child boxes that are trakBox objects + /// + public IEnumerable Tracks + { + get + { + return Children.Where((box) => box is trakBox).Cast(); + } + } + + /// + /// Returns the optional extended movie header for this movie. + /// + public mvexBox? ExtendedHeader + { + get + { + return Children.Where((box) => box is mvexBox).SingleOrDefault() as mvexBox; + } + } + + } +} diff --git a/fmp4/mvexBox.cs b/fmp4/mvexBox.cs new file mode 100644 index 0000000..33f156c --- /dev/null +++ b/fmp4/mvexBox.cs @@ -0,0 +1,49 @@ + +using System.Diagnostics; + +namespace AMSMigrate.Fmp4 +{ + /// + /// An object model representation for the move extended header object. + /// + public class mvexBox : Box + { + /// + /// Default constructor for the box. + /// + public mvexBox() : + base(MP4BoxType.mvex) + { + } + + /// + /// Deserializing/copy constructor. + /// + /// the box to construct from + public mvexBox(Box box): + base(box) + { + Debug.Assert(box.Type == MP4BoxType.mvex); + } + + /// + /// Pases the body of box. A mvex is nothing but a collection of child boxes. + /// + protected override void ReadBody() + { + ReadChildren(); + } + + /// + /// Returns a collection of child boxes that are 'trex' boxes + /// + public IEnumerable Tracks + { + get + { + return Children.Where((box) => box is trexBox).Cast(); + } + } + + } +} diff --git a/fmp4/mvhdBox.cs b/fmp4/mvhdBox.cs new file mode 100644 index 0000000..0067567 --- /dev/null +++ b/fmp4/mvhdBox.cs @@ -0,0 +1,359 @@ + +using System.Diagnostics; +using System.Globalization; + +namespace AMSMigrate.Fmp4 +{ + /// + /// Represents a movie header box . ISO 14496-12 Sec:8.2.2 + /// + public class mvhdBox : FullBox, IEquatable + { + /// + /// Default constructor + /// + public mvhdBox() : + base(version:0, flags:0, boxtype:MP4BoxType.mvhd) + { + _nextTrackId = UInt32.MaxValue; + Matrix = new Int32[9]; + } + + /// + /// Copy constructor + /// + /// the box to copy from + public mvhdBox(Box box): + base(box) + { + Debug.Assert(box.Type == MP4BoxType.mvhd); + Matrix = new Int32[9]; + } + + /// + /// Get or set the next available track ID. + /// + public UInt32 NextTrackId + { + get + { + return _nextTrackId; + } + set + { + _nextTrackId = value; + SetDirty(); + } + } + + /// + /// Creation time of media as number of seconds since 1st Jan 1904. + /// + public UInt64 CreationTime { get; private set; } + + /// + /// Creation time of media as number of seconds since 1st Jan 1904. + /// + public UInt64 ModificationTime { get; private set; } + + /// + /// TimeScale of the track. + /// + public UInt32 TimeScale { get; private set; } + + /// + /// Duration of the track in the timescale. + /// + public UInt64 Duration { get; private set; } + + /// + /// The rate of video playback. + /// + public UInt32 Rate { get; private set; } + + /// + /// Volume for an audio track. + /// + public UInt16 Volume { get; private set; } + + /// + /// Transformation matrix to apply to a video. + /// + public Int32[] Matrix { get; private set; } + + /// + /// The next available track ID. + /// + private UInt32 _nextTrackId; + + /// + /// Parse the body contents of this box. + /// + /// + protected override void ReadBody() + { + base.ReadBody(); + + long startPosition = Body!.BaseStream.Position; + + try + { + + if (Version == 1) + { + CreationTime = Body.ReadUInt64(); + ModificationTime = Body.ReadUInt64(); + TimeScale = Body.ReadUInt32(); + Duration = Body.ReadUInt64(); + } + else if (Version == 0) + { + CreationTime = Body.ReadUInt32(); + ModificationTime = Body.ReadUInt32(); + TimeScale = Body.ReadUInt32(); + Duration = Body.ReadUInt32(); + SetDirty(); //We can't round trip version 0. + } + else + { + throw new MP4DeserializeException( + TypeDescription, + -BodyPreBytes, + BodyInitialOffset, + String.Format(CultureInfo.InvariantCulture, "The version field for an mvhd box must be either 0 or 1. Instead found {0}", + Version)); + } + + Rate = Body.ReadUInt32(); + Volume = Body.ReadUInt16(); + + UInt32 reserved = Body.ReadUInt16(); + Debug.Assert(reserved == 0); + + for (int i = 0; i < 2; ++i) + { + reserved = Body.ReadUInt32(); + Debug.Assert(reserved == 0); + } + + Matrix = new Int32[9]; + for (int i = 0; i < Matrix.Length; ++i) + { + Matrix[i] = Body.ReadInt32(); + } + + for (int i = 0; i < 6; ++i) + { + reserved = Body.ReadUInt32(); + Debug.Assert(reserved == 0); + } + + _nextTrackId = Body.ReadUInt32(); + } + catch (EndOfStreamException ex) + { + // Reported error offset will point to start of box + throw new MP4DeserializeException(TypeDescription, -BodyPreBytes, BodyInitialOffset, + String.Format(CultureInfo.InvariantCulture, "Could not read mvhd fields, only {0} bytes left in reported size, expected {1}", + Body.BaseStream.Length - startPosition, ComputeLocalSize()), ex); + } + + } + + /// + /// serialize the contents of the box + /// + /// MP4Writer to write to + protected override void WriteToInternal(MP4Writer writer) + { + //Version is always 1. + Version = 1; + base.WriteToInternal(writer); + writer.WriteUInt64(CreationTime); + writer.WriteUInt64(ModificationTime); + writer.WriteUInt32(TimeScale); + writer.WriteUInt64(Duration); + writer.WriteUInt32(Rate); + writer.WriteUInt16(Volume); + writer.WriteUInt16(0); + for(int i =0; i < 2; ++i) + { + writer.WriteUInt32(0); + } + + foreach(Int32 value in Matrix) + { + writer.WriteInt32(value); + } + + for (int i = 0; i < 6; ++i) + { + writer.WriteUInt32(0); + } + + writer.WriteUInt32(_nextTrackId); + } + + /// + /// Compute the size of the box. + /// + /// size of the box itself + public override UInt64 ComputeSize() + { + return base.ComputeSize() + ComputeLocalSize(); + } + + /// + /// computes the size of just box specific content. + /// + /// + protected static new UInt64 ComputeLocalSize() + { + return 8 //creation time + + 8 //modification time + + 4 //timescale + + 8 //duration + + 4 //rate + + 2 //volume + + 2 + 4 * 2 // reserved bits. + + 9 * 4 // size of matrix + + 4 * 6 //reserved + + 4; + } + + #region equality methods + /// + /// Compare this box to another object. + /// + /// the object to compare equality against + /// + public override bool Equals(Object? other) + { + return this.Equals(other as mvhdBox); + } + + /// + /// Returns the hash code of the object. + /// + /// + public override int GetHashCode() + { + return base.GetHashCode(); + } + + /// + /// Box.Equals override. This is done so that programs which attempt to test equality + /// using pointers to Box will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public override bool Equals(Box? other) + { + return this.Equals(other as mvhdBox); + } + + /// + /// FullBox.Equals override. This is done so that programs which attempt to test equality + /// using pointers to Box will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public override bool Equals(FullBox? other) + { + return this.Equals(other as mvhdBox); + } + + /// + /// Compare two mvhdBoxes for equality. + /// + /// other mvhdBox to compare against + /// + public bool Equals(mvhdBox? other) + { + if (!base.Equals((FullBox?)other)) + return false; + + if (CreationTime != other.CreationTime) + { + return false; + } + + if (ModificationTime != other.ModificationTime) + { + return false; + } + + if (TimeScale != other.TimeScale) + { + return false; + } + + if (Duration != other.Duration) + { + return false; + } + + if (Rate != other.Rate) + { + return false; + } + + if (Volume != other.Volume) + { + return false; + } + + if (_nextTrackId != other._nextTrackId) + { + return false; + } + + if (!Matrix.SequenceEqual(other.Matrix)) + { + return false; + } + + return true; + + } + + /// + /// Compare two mvhdBox objects for equality. + /// + /// left hand side of == + /// right hand side of == + /// + public static bool operator ==(mvhdBox? lhs, mvhdBox? rhs) + { + // Check for null on left side. + if (Object.ReferenceEquals(lhs, null)) + { + if (Object.ReferenceEquals(rhs, null)) + { + // null == null = true. + return true; + } + + // Only the left side is null. + return false; + } + return lhs.Equals(rhs); + } + + /// + /// Compare two mvhdBox objects for equality + /// + /// left side of != + /// right side of != + /// true if not equal else false. + public static bool operator !=(mvhdBox? lhs, mvhdBox? rhs) + { + return !(lhs == rhs); + } + + + #endregion + } +} diff --git a/fmp4/sdtpBox.cs b/fmp4/sdtpBox.cs new file mode 100644 index 0000000..38d53b0 --- /dev/null +++ b/fmp4/sdtpBox.cs @@ -0,0 +1,232 @@ + +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Globalization; + +namespace AMSMigrate.Fmp4 +{ + + /// + /// An object that represent the sdtp box in an ISO file as per 14496-12 + /// + public class sdtpBox : FullBox, IEquatable + { + /// + /// Default constructor. + /// + public sdtpBox() : + base(version:0, flags:0, boxtype:MP4BoxType.sdtp) + { + Size.Value = ComputeSize(); + _entries.CollectionChanged += _dependencies_CollectionChanged; + } + + /// + /// Deserializing/copy constructor. + /// + /// the box to construct from + public sdtpBox(Box box): + base(box) + { + Debug.Assert(box.Type == MP4BoxType.sdtp); + + _entries.CollectionChanged += _dependencies_CollectionChanged; + } + + /// + /// If the collection of sample dependencies is modified marks the box dirty. + /// + /// + /// + void _dependencies_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + SetDirty(); + } + + /// + /// Pases the body of box. A moof is nothing but a collection of child boxes. + /// + protected override void ReadBody() + { + base.ReadBody(); + + // Check on version - we expect 0 , reject anything else + if (Version != 0) + { + // Reported error offset will point to start of box + throw new MP4DeserializeException(TypeDescription, -BodyPreBytes, BodyInitialOffset, + String.Format(CultureInfo.InvariantCulture, "Unexpected version: {0}. Expected 0!", Version)); + } + + // Check on version - we expect 0 , reject anything else + if (Flags != 0) + { + // Reported error offset will point to start of box + throw new MP4DeserializeException(TypeDescription, -BodyPreBytes, BodyInitialOffset, + String.Format(CultureInfo.InvariantCulture, "Unexpected Flags: {0}. Expected 0!", Flags)); + } + + //Read till the end of the body. + while (Body!.BaseStream.Position < Body.BaseStream.Length) + { + _entries.Add(new sdtpEntry(Body.ReadByte())); + } + } + + /// + /// Serialize the box contents to an MP4Writer + /// + /// the MP4Writer to serialize the box. + protected override void WriteToInternal(MP4Writer writer) + { + base.WriteToInternal(writer); + foreach (sdtpEntry dependency in _entries) + { + writer.Write(dependency.Value); + } + } + + + /// + /// Calculate the current size of this box. + /// + /// The current size of this box, if it were to be written to disk now. + public override UInt64 ComputeSize() + { + return base.ComputeSize() + ComputeLocalSize(); + } + + /// + /// Calculates the current size of just this class (base classes excluded). This is called by + /// ComputeSize(). + /// + /// The current size of just the fields from this box. + protected new UInt64 ComputeLocalSize() + { + return (UInt64)_entries.Count; + } + + + /// + /// A collection of sample dependency entries one per sample.. + /// + public IList Entries + { + get + { + return _entries; + } + } + + /// + /// An internal collection of SampleDependency entries. + /// + private ObservableCollection _entries = new ObservableCollection(); + + #region equality methods. + + /// + /// Object.Equals override. + /// + /// The object to test equality against. + /// True if the this and the given object are equal. + public override bool Equals(object? obj) + { + return this.Equals(obj as sdtpBox); + } + + /// + /// Object.GetHashCode override. This must be done as a consequence of overridding + /// Object.Equals. + /// + /// Hash code which will be match the hash code of an object which is equal. + public override int GetHashCode() + { + return base.GetHashCode(); + } + + /// + /// Box.Equals override. This is done so that programs which attempt to test equality + /// using pointers to Box will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public override bool Equals(Box? other) + { + return this.Equals(other as sdtpBox); + } + + /// + /// FullBox.Equals override. This is done so that programs which attempt to test equality + /// using pointers to Box will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public override bool Equals(FullBox? other) + { + return this.Equals(other as sdtpBox); + } + + /// + /// compare the box with another sdtpBox + /// + /// other sdtpBox to compare with + /// + public bool Equals(sdtpBox? other) + { + if (!base.Equals(other as FullBox)) + { + return false; + } + + //Each element in the sequence should be equal. + if (!_entries.SequenceEqual(other._entries)) + { + return false; + } + + return true; + } + + /// + /// Compare two sdtpBox objects for equality using Equals() method. + /// + /// left side of == + /// right side of == + /// true if the two boxes are equal else false. + public static bool operator ==(sdtpBox? lhs, sdtpBox? rhs) + { + // Check for null on left side. + if (Object.ReferenceEquals(lhs, null)) + { + if (Object.ReferenceEquals(rhs, null)) + { + // null == null = true. + return true; + } + + // Only the left side is null. + return false; + } + return lhs.Equals(rhs); + } + + /// + /// compares two entries using the Equals() method for equality. + /// + /// left side of != + /// right side of != + /// return true if two boxes are not equal else false. + public static bool operator !=(sdtpBox? lhs, sdtpBox? rhs) + { + return !(lhs == rhs); + } + + + #endregion + + } +} diff --git a/fmp4/sdtpEntry.cs b/fmp4/sdtpEntry.cs new file mode 100644 index 0000000..a280c0f --- /dev/null +++ b/fmp4/sdtpEntry.cs @@ -0,0 +1,244 @@ + + +namespace AMSMigrate.Fmp4 +{ + /// + /// An enumeration to describe whether the sample depends on other samples. + /// + public enum SampleDependsOn : byte + { + Unknown = 0, + DependsOnOthers = 1, + NotDependsOnOthers = 2 + }; + + /// + /// An enumeration to describe whether other samples depends on this sample. + /// + public enum SampleIsDependedOn : byte + { + Unknown = 0, + NotDisposable = 1, + Disposable = 2 + }; + + /// + /// An enumeration to describe whether the sample has redundancy. + /// + public enum SampleRedundancy : byte + { + Unknown = 0, //it is unknown whether there is redundant coding in this sample; + Redundant = 1, //there is redundant coding in this sample; + NotRedundant = 2 //there is no redundant coding in this sample; + }; + + /// + /// An enumeration to describe whether the sample is leading. + /// + public enum SampleIsLeading : byte + { + Unknown = 0, + Leading = 1, + NotLeading = 2, + LeadingButDecodable = 3 + }; + + /// + /// A value type to represent a sample dependency info; + /// + public class sdtpEntry : IEquatable + { + /// + /// Default constructor. + /// + public sdtpEntry() + { + } + + /// + /// Public Constructor. + /// + /// + public sdtpEntry(byte dependencyValue) + { + _value = dependencyValue; + } + + /// + /// Deep copy constructor (only contains value type, so everything is deep). + /// + /// The instance to copy from. + public sdtpEntry(sdtpEntry other) + { + _value = other._value; + } + + /// + /// The redundancy information for this entry. + /// + public SampleRedundancy Redundancy + { + get + { + return (SampleRedundancy)(_value & 0x3); + } + set + { + //clear the old bits and set the new bits. + _value &= 0xFC; + _value |= (byte)value; + } + } + + /// + /// Indicates whether other samples depend on this sample. + /// + public SampleIsDependedOn DependedOn + { + get + { + return (SampleIsDependedOn)((_value & 0xC) >> 2); + } + set + { + _value &= 0XF3; + _value |= (byte)((byte)value << 2); + } + } + + /// + /// Indicates whether this sample depends on others or not. + /// + public SampleDependsOn DependsOn + { + get + { + return (SampleDependsOn)((_value & 0x30) >> 4); + } + set + { + _value &= 0XCF; + _value |= (byte)((byte)value << 4); + } + } + + + /// + /// Indicates whether this sample is leading or not. + /// + public SampleIsLeading Leading + { + get + { + return (SampleIsLeading)((_value & 0xC0) >> 6); + } + set + { + _value &= 0X3F; + _value |= (byte)((byte)value << 6); + } + } + /// + /// Returns the byte value of dependency. + /// + public byte Value + { + get + { + return _value; + } + } + + + /// + /// The value of the dependency information. + /// + private byte _value; + + #region equality methods + + /// + /// Compares this entry with another object. + /// + /// + /// + public override bool Equals(Object? obj) + { + return this.Equals(obj as sdtpEntry); + } + + /// + /// Compares this sdtpEntry with another sdtpEntry. + /// + /// + /// + public bool Equals(sdtpEntry? other) + { + // If parameter is null, return false. + if (Object.ReferenceEquals(other, null)) + { + return false; + } + + // Optimization for a common success case. + if (Object.ReferenceEquals(this, other)) + { + return true; + } + + // If run-time types are not exactly the same, return false. + if (this.GetType() != other.GetType()) + return false; + + return (_value == other._value); + } + + /// + /// Returns the hash code for this object. + /// + /// + public override int GetHashCode() + { + return _value; + } + + /// + /// Compare two sdtpEntries for equality. + /// + /// left side of == + /// right side of == + /// true if the two entries are equal else false. + public static bool operator ==(sdtpEntry? lhs, sdtpEntry? rhs) + { + // Check for null on left side. + if (Object.ReferenceEquals(lhs, null)) + { + if (Object.ReferenceEquals(rhs, null)) + { + // null == null = true. + return true; + } + + // Only the left side is null. + return false; + } + // Equals handles case of null on right side. + return lhs.Equals(rhs); + } + + /// + /// compares two entries using the Equals() method for equality. + /// + /// left side of != + /// right side of != + /// return true if two entries are not equal else false. + public static bool operator !=(sdtpEntry? lhs, sdtpEntry? rhs) + { + return !(lhs == rhs); + } + + #endregion + + }; + +} diff --git a/fmp4/tfdtBox.cs b/fmp4/tfdtBox.cs new file mode 100644 index 0000000..3bb89e1 --- /dev/null +++ b/fmp4/tfdtBox.cs @@ -0,0 +1,210 @@ + +using System.Diagnostics; +using System.Globalization; + +namespace AMSMigrate.Fmp4 +{ + /// + /// Represents a Track fragment decode time box . ISO 14496-12 Sec:8.8.12 + /// + public class tfdtBox : FullBox, IEquatable + { + /// + /// Default constructor with a decode time. + /// + /// The decode time which this box refers to. + public tfdtBox(UInt64 decodeTime) : + base(version: 1, flags: 0, boxtype: MP4BoxType.tfdt) + { + _decodeTime = decodeTime; + } + + /// + /// Copy constructor + /// + /// the box to copy from + public tfdtBox(Box box) : + base(box) + { + Debug.Assert(box.Type == MP4BoxType.tfdt); + } + + /// + /// Get or set the decode time for the chunk. + /// + public UInt64 DecodeTime + { + get + { + return _decodeTime; + } + set + { + _decodeTime = value; + SetDirty(); + } + } + + public UInt64 _decodeTime; + + /// + /// Parse the body contents of this box. + /// + /// + protected override void ReadBody() + { + base.ReadBody(); + + long startPosition = Body!.BaseStream.Position; + + try + { + if (Version == 1) + { + _decodeTime = Body.ReadUInt64(); + } + else if (Version == 0) + { + _decodeTime = Body.ReadUInt32(); + } + else + { + throw new MP4DeserializeException( + TypeDescription, + -BodyPreBytes, + BodyInitialOffset, + String.Format(CultureInfo.InvariantCulture, "The version field for a tfdt box must be either 0 or 1. Instead found {0}", + Version)); + } + } + catch (EndOfStreamException ex) + { + // Reported error offset will point to start of box + throw new MP4DeserializeException(TypeDescription, -BodyPreBytes, BodyInitialOffset, + String.Format(CultureInfo.InvariantCulture, "Could not read tfdt fields, only {0} bytes left in reported size, expected {1}", + Body.BaseStream.Length - startPosition, ComputeLocalSize()), ex); + } + } + + protected override void WriteToInternal(MP4Writer writer) + { + //Version is always 1. + Version = 1; + base.WriteToInternal(writer); + + writer.WriteUInt64(_decodeTime); + } + + /// + /// Compute the size of the box. + /// + /// size of the box itself + public override UInt64 ComputeSize() + { + return base.ComputeSize() + ComputeLocalSize(); + } + + protected static new UInt64 ComputeLocalSize() + { + return 8; // Decode Time + } + + #region equality methods + /// + /// Compare this box to another object. + /// + /// the object to compare equality against + /// + public override bool Equals(Object? other) + { + return this.Equals(other as tfdtBox); + } + + /// + /// Returns the hash code of the object. + /// + /// + public override int GetHashCode() + { + return base.GetHashCode(); + } + + /// + /// Box.Equals override. This is done so that programs which attempt to test equality + /// using pointers to Box will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public override bool Equals(Box? other) + { + return this.Equals(other as tfdtBox); + } + + /// + /// FullBox.Equals override. This is done so that programs which attempt to test equality + /// using pointers to Box will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public override bool Equals(FullBox? other) + { + return this.Equals(other as tfdtBox); + } + + /// + /// Compare two tfdtBoxes for equality. + /// + /// other tfdtBox to compare against + /// + public bool Equals(tfdtBox? other) + { + if (!base.Equals((FullBox?)other)) + return false; + + if (DecodeTime != other.DecodeTime) + { + return false; + } + + return true; + } + + /// + /// Compare two tfdtBox objects for equality. + /// + /// left hand side of == + /// right hand side of == + /// + public static bool operator ==(tfdtBox? lhs, tfdtBox? rhs) + { + // Check for null on left side. + if (Object.ReferenceEquals(lhs, null)) + { + if (Object.ReferenceEquals(rhs, null)) + { + // null == null = true. + return true; + } + + // Only the left side is null. + return false; + } + return lhs.Equals(rhs); + } + + /// + /// Compare two tfdtBox objects for equality + /// + /// left side of != + /// right side of != + /// true if not equal else false. + public static bool operator !=(tfdtBox? lhs, tfdtBox? rhs) + { + return !(lhs == rhs); + } + + #endregion + } +} diff --git a/fmp4/tfhdBox.cs b/fmp4/tfhdBox.cs new file mode 100644 index 0000000..d2de6b7 --- /dev/null +++ b/fmp4/tfhdBox.cs @@ -0,0 +1,471 @@ + +using System.Diagnostics; +using System.Globalization; + +namespace AMSMigrate.Fmp4 +{ + /// + /// Represents a track fragment header box. ISO 14496-12 Sec:8.35.2 + /// + public class tfhdBox : FullBox, IEquatable + { + /// + /// Default constructor with a given track id. + /// + /// + public tfhdBox(UInt32 trackId) : + base(version:0, flags:0, boxtype:MP4BoxType.tfhd) + { + _trackId = trackId; + Size.Value = ComputeSize(); + } + + /// + /// Copy constructor (deep copy, all properties are ValueType). + /// + /// The tfhdBox to copy. + public tfhdBox(tfhdBox other) : + base (other.Version, other.Flags, boxtype: MP4BoxType.tfhd) + { + _trackId = other._trackId; + _baseOffset = other._baseOffset; + _sampleDescriptionIndex = other._sampleDescriptionIndex; + _defaultSampleSize = other._defaultSampleSize; + _defaultSampleDuration = other._defaultSampleDuration; + _defaultSampleFlags = other._defaultSampleFlags; + // Policy is for Dirty to be true, which base should already be doing + } + + /// + /// deserializing constructor. + /// + /// + public tfhdBox(Box box): + base(box) + { + Debug.Assert(box.Type == MP4BoxType.tfhd); + } + + /// + /// An enum to defined the various allowed flags for a tfhd box. + /// + [Flags] + public enum TfhdFlags : uint + { + None = 0, + BaseDataOffsetPresent = 0x1, + SampleDescriptionIndexPresent = 0x2, + DefaultSampleDurationPresent = 0x8, + DefaultSampleSizePresent = 0x10, + DefaultSampleFlagsPresent = 0x20, + DurationIsEmpty = 0x010000 + } + + /// + /// The track ID of the track present in the fragment. + /// + public UInt32 TrackId + { + get + { + return _trackId; + } + set + { + _trackId = value; + SetDirty(); + } + } + + /// + /// The offset of the moof box for this fragment in the file. + /// + public UInt64? BaseOffset + { + get + { + return _baseOffset; + } + set + { + _baseOffset = value; + SetDirty(); + } + } + + /// + /// Optional sample description index for this track. + /// + public UInt32? SampleDescriptionIndex + { + get + { + return _sampleDescriptionIndex; + } + set + { + _sampleDescriptionIndex = value; + SetDirty(); + } + } + + /// + /// Optional default sample size for this fragment. + /// + public UInt32? DefaultSampleSize + { + get + { + return _defaultSampleSize; + } + set + { + _defaultSampleSize = value; + SetDirty(); + } + } + + /// + /// Optional default sample duration for this fragment. + /// + public UInt32? DefaultSampleDuration + { + get + { + return _defaultSampleDuration; + } + set + { + _defaultSampleDuration = value; + SetDirty(); + } + } + + /// + /// Optional default sample flags for this fragment. + /// + public UInt32? DefaultSampleFlags + { + get + { + return _defaultSampleFlags; + } + set + { + _defaultSampleFlags = value; + SetDirty(); + } + } + + /// + /// Parses the body of the box. + /// + protected override void ReadBody() + { + base.ReadBody(); + + TfhdFlags flags = (TfhdFlags)(Flags); + long startPosition = Body!.BaseStream.Position; + + try + { + _trackId = Body.ReadUInt32(); + + if ((flags & TfhdFlags.BaseDataOffsetPresent) != 0) + { + _baseOffset = Body.ReadUInt64(); + } + + if ((flags & TfhdFlags.SampleDescriptionIndexPresent) != 0) + { + _sampleDescriptionIndex = Body.ReadUInt32(); + } + + if ((flags & TfhdFlags.DefaultSampleDurationPresent) != 0) + { + _defaultSampleDuration = Body.ReadUInt32(); + } + + if ((flags & TfhdFlags.DefaultSampleSizePresent) != 0) + { + _defaultSampleSize = Body.ReadUInt32(); + } + + if ((flags & TfhdFlags.DefaultSampleFlagsPresent) != 0) + { + _defaultSampleFlags = Body.ReadUInt32(); + } + } + catch (EndOfStreamException ex) + { + // Reported error offset will point to start of box + throw new MP4DeserializeException(TypeDescription, -BodyPreBytes, BodyInitialOffset, + String.Format(CultureInfo.InvariantCulture, "Could not read tfhd fields, only {0} bytes left in reported size, expected {1}", + Body.BaseStream.Length - startPosition, ComputeLocalSize()), ex); + } + + } + + /// + /// Serialize the box contents to the writer. + /// + /// the MP4Writer to write to + protected override void WriteToInternal(MP4Writer writer) + { + if (Dirty) + { + //Update the flags. + Flags = ComputeFlags(); + } + + base.WriteToInternal(writer); + + writer.WriteUInt32(_trackId); + + + if (_baseOffset.HasValue) + { + writer.WriteUInt64(_baseOffset.Value); + } + + if (_sampleDescriptionIndex.HasValue) + { + writer.WriteUInt32(_sampleDescriptionIndex.Value); + } + + if (_defaultSampleDuration.HasValue) + { + writer.WriteUInt32(_defaultSampleDuration.Value); + } + + if (_defaultSampleSize.HasValue) + { + writer.WriteUInt32(_defaultSampleSize.Value); + } + + if (_defaultSampleFlags.HasValue) + { + writer.WriteUInt32(_defaultSampleFlags.Value); + } + } + + /// + /// Computes the overall size of the box. + /// + /// + public override ulong ComputeSize() + { + return base.ComputeSize() + ComputeLocalSize(); + } + + /// + /// Compute the size of box specific members. + /// + /// + protected new UInt64 ComputeLocalSize() + { + UInt64 thisSize = 4; //track id. + + if (_baseOffset.HasValue) + { + thisSize += 8; + } + + if (_sampleDescriptionIndex.HasValue) + { + thisSize += 4; + } + + if (_defaultSampleSize.HasValue) + { + thisSize += 4; + } + + if (_defaultSampleDuration.HasValue) + { + thisSize += 4; + } + + if (_defaultSampleFlags.HasValue) + { + thisSize += 4; + } + + return thisSize; + } + + /// + /// Helper method to compute the flags based on values present. + /// + /// The flags to set. + private UInt32 ComputeFlags() + { + TfhdFlags flags = TfhdFlags.None; + + if (_baseOffset.HasValue) + { + flags |= TfhdFlags.BaseDataOffsetPresent; + } + + if (_sampleDescriptionIndex.HasValue) + { + flags |= TfhdFlags.SampleDescriptionIndexPresent; + } + + if (_defaultSampleSize.HasValue) + { + flags |= TfhdFlags.DefaultSampleSizePresent; + } + + if (_defaultSampleDuration.HasValue) + { + flags |= TfhdFlags.DefaultSampleDurationPresent; + } + + if (_defaultSampleFlags.HasValue) + { + flags |= TfhdFlags.DefaultSampleFlagsPresent; + } + + return (UInt32)flags; + + } + /// + /// Track id of the track + /// + private UInt32 _trackId; + + /// + /// base offset of the moof box in the file. + /// + private UInt64? _baseOffset; + + /// + /// The sample description index for this track. + /// + private UInt32? _sampleDescriptionIndex; + + /// + /// default sample size. + /// + private UInt32? _defaultSampleSize; + + /// + /// default sample duration. + /// + private UInt32? _defaultSampleDuration; + + /// + /// default sample flags. + /// + private UInt32? _defaultSampleFlags; + + #region equality methods + + /// + /// Compare this object with another object. + /// + /// other object to compare with. + /// true if both are equal else false + public override bool Equals(Object? other) + { + return this.Equals(other as tfhdBox); + } + + /// + /// Returns the hashcode for this object. + /// + /// + public override int GetHashCode() + { + return base.GetHashCode(); + } + + /// + /// Box.Equals override. This is done so that programs which attempt to test equality + /// using pointers to Box will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public override bool Equals(Box? other) + { + return this.Equals(other as tfhdBox); + } + + /// + /// FullBox.Equals override. This is done so that programs which attempt to test equality + /// using pointers to Box will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public override bool Equals(FullBox? other) + { + return this.Equals(other as tfhdBox); + } + + /// + /// Compare this object with another tfhdBox + /// + /// the other box to compare against + /// true if both are equal else false + public bool Equals(tfhdBox? other) + { + if (!base.Equals((FullBox?)other)) + { + return false; + } + + //check the class specific members. + if (_trackId != other._trackId || + _baseOffset != other._baseOffset || + _sampleDescriptionIndex != other._sampleDescriptionIndex || + _defaultSampleSize != other._defaultSampleSize || + _defaultSampleDuration != other._defaultSampleDuration || + _defaultSampleFlags != other._defaultSampleFlags + ) + { + return false; + } + + return true; + } + + /// + /// Compare two tfhdBox objects for equality. + /// + /// left side of == + /// right side of == + /// true if the two boxes are equal else false. + public static bool operator ==(tfhdBox? lhs, tfhdBox? rhs) + { + // Check for null on left side. + if (Object.ReferenceEquals(lhs, null)) + { + if (Object.ReferenceEquals(rhs, null)) + { + // null == null = true. + return true; + } + + // Only the left side is null. + return false; + } + return lhs.Equals(rhs); + } + + /// + /// compares two entries using the Equals() method for equality. + /// + /// left side of != + /// right side of != + /// return true if two boxes are not equal else false. + public static bool operator !=(tfhdBox? lhs, tfhdBox? rhs) + { + return !(lhs == rhs); + } + + #endregion + } +} diff --git a/fmp4/tkhdBox.cs b/fmp4/tkhdBox.cs new file mode 100644 index 0000000..40ad8c8 --- /dev/null +++ b/fmp4/tkhdBox.cs @@ -0,0 +1,343 @@ + +using System.Diagnostics; +using System.Globalization; + +namespace AMSMigrate.Fmp4 +{ + /// + /// Represents a movie header box . ISO 14496-12 Sec:8.2.2 + /// + public class tkhdBox : FullBox, IEquatable + { + /// + /// Default constructor with a track id + /// + /// The track ID of the track.. + public tkhdBox(UInt32 trackId) : + base(version: 0, flags: 0, boxtype: MP4BoxType.tkhd) + { + Matrix = new Int32[9]; + _trackId = trackId; + } + + /// + /// Copy constructor + /// + /// the box to copy from + public tkhdBox(Box box) : + base(box) + { + Debug.Assert(box.Type == MP4BoxType.tkhd); + Matrix = new Int32[9]; + } + + /// + /// Get or set the track ID for a track. + /// + public UInt32 TrackId + { + get + { + return _trackId; + } + set + { + _trackId = value; + SetDirty(); + } + } + + /// + /// Creation time of media as number of seconds since 1st Jan 1904. + /// + public UInt64 CreationTime { get; private set; } + + /// + /// Creation time of media as number of seconds since 1st Jan 1904. + /// + public UInt64 ModificationTime { get; private set; } + + /// + /// Duration of the track in the timescale. + /// + public UInt64 Duration { get; private set; } + + /// + /// The z-axis of the video track. + /// + public UInt16 Layer { get; private set; } + + /// + /// The alternate group to which this track belongs. + /// + public UInt16 AlternateGroup { get; private set; } + + /// + /// Volume for an audio track. + /// + public UInt16 Volume { get; private set; } + + /// + /// The optional transformation matrix to be used for the video. + /// + public Int32[] Matrix { get ; private set; } + + /// + /// The current track ID. + /// + private UInt32 _trackId; + + + /// + /// The width of a video track. + /// + public UInt32 Width { get; private set; } + + /// + /// The height of a video track. + /// + public UInt32 Height { get; private set; } + + /// + /// Parse the body contents of this box. + /// + /// + protected override void ReadBody() + { + base.ReadBody(); + + long startPosition = Body!.BaseStream.Position; + + try + { + UInt32 reserved; + + if (Version == 1) + { + CreationTime = Body.ReadUInt64(); + ModificationTime = Body.ReadUInt64(); + _trackId = Body.ReadUInt32(); + reserved = Body.ReadUInt32(); + Debug.Assert(reserved == 0); + Duration = Body.ReadUInt64(); + } + else + { + CreationTime = Body.ReadUInt32(); + ModificationTime = Body.ReadUInt32(); + _trackId = Body.ReadUInt32(); + reserved = Body.ReadUInt32(); + Debug.Assert(reserved == 0); + Duration = Body.ReadUInt32(); + SetDirty(); //We can't round trip version 0. + } + + for (int i = 0; i < 2; ++i) + { + reserved = Body.ReadUInt32(); + Debug.Assert(reserved == 0); + } + + Layer = Body.ReadUInt16(); + AlternateGroup = Body.ReadUInt16(); + Volume = Body.ReadUInt16(); + + reserved = Body.ReadUInt16(); + Debug.Assert(reserved == 0); + + Matrix = new Int32[9]; + for (int i = 0; i < Matrix.Length; ++i) + { + Matrix[i] = Body.ReadInt32(); + } + + Width = Body.ReadUInt32(); + Height = Body.ReadUInt32(); + + } + catch (EndOfStreamException ex) + { + // Reported error offset will point to start of box + throw new MP4DeserializeException(TypeDescription, -BodyPreBytes, BodyInitialOffset, + String.Format(CultureInfo.InvariantCulture, "Could not read tkhd fields, only {0} bytes left in reported size, expected {1}", + Body.BaseStream.Length - startPosition, ComputeLocalSize()), ex); + } + + } + + + /// + /// serialize the contents of the box to a writer. + /// + /// MP4Writer to write to + protected override void WriteToInternal(MP4Writer writer) + { + //Version is always 1. + Version = 1; + base.WriteToInternal(writer); + writer.WriteUInt64(CreationTime); + writer.WriteUInt64(ModificationTime); + writer.WriteUInt32(TrackId); + writer.WriteUInt32(0); + writer.WriteUInt64(Duration); + for (int i = 0; i < 2; ++i) + { + writer.WriteUInt32(0); + } + writer.WriteUInt16(Layer); + writer.WriteUInt16(AlternateGroup); + writer.WriteUInt16(Volume); + writer.WriteUInt16(0); + + foreach(Int32 value in Matrix) + { + writer.WriteInt32(value); + } + + writer.WriteUInt32(Width); + writer.WriteUInt32(Height); + } + + /// + /// Compute the size of the box. + /// + /// size of the box itself + public override UInt64 ComputeSize() + { + return base.ComputeSize() + ComputeLocalSize(); + } + + protected static new UInt64 ComputeLocalSize() + { + return 8 //creation time + + 8 //modification time + + 4 //track id + + 4 //reserved + + 8 //duration + + 2 * 4 // reserved + + 2 //layer + + 2 //alternate group + + 2 //volume + + 2 // reserved bits. + + 9 * 4 // size of matrix + + 4 //width + + 4; //height + } + + + #region equality methods + /// + /// Compare this box to another object. + /// + /// the object to compare equality against + /// + public override bool Equals(Object? other) + { + return this.Equals(other as tkhdBox); + } + + /// + /// Returns the hascode of the object. + /// + /// + public override int GetHashCode() + { + return base.GetHashCode(); + } + + /// + /// Box.Equals override. This is done so that programs which attempt to test equality + /// using pointers to Box will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public override bool Equals(Box? other) + { + return this.Equals(other as tkhdBox); + } + + /// + /// FullBox.Equals override. This is done so that programs which attempt to test equality + /// using pointers to Box will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public override bool Equals(FullBox? other) + { + return this.Equals(other as tkhdBox); + } + + /// + /// Compare two tkhdBoxes for equality. + /// + /// other tkhdBox to compare against + /// + public bool Equals(tkhdBox? other) + { + if (!base.Equals((FullBox?)other)) + return false; + + if (CreationTime != other.CreationTime) + { + return false; + } + + if (ModificationTime != other.ModificationTime) + { + return false; + } + + if (TrackId != other.TrackId) + { + return false; + } + + if (Duration != other.Duration) + { + return false; + } + + + return true; + } + + /// + /// Compare two tkhdBox objects for equality. + /// + /// left hand side of == + /// right hand side of == + /// + public static bool operator ==(tkhdBox? lhs, tkhdBox? rhs) + { + // Check for null on left side. + if (Object.ReferenceEquals(lhs, null)) + { + if (Object.ReferenceEquals(rhs, null)) + { + // null == null = true. + return true; + } + + // Only the left side is null. + return false; + } + return lhs.Equals(rhs); + } + + /// + /// Compare two tkhdBox objects for equality + /// + /// left side of != + /// right side of != + /// true if not equal else false. + public static bool operator !=(tkhdBox? lhs, tkhdBox? rhs) + { + return !(lhs == rhs); + } + + + #endregion + } +} diff --git a/fmp4/trafBox.cs b/fmp4/trafBox.cs new file mode 100644 index 0000000..a6a214a --- /dev/null +++ b/fmp4/trafBox.cs @@ -0,0 +1,142 @@ + +using System.Diagnostics; + +namespace AMSMigrate.Fmp4 +{ + /// + /// An object model for the 'traf' box from ISO 14496-12. + /// + public class trafBox : Box, IEquatable + { + + /// + /// Default constructor. + /// + public trafBox() : + base(MP4BoxType.traf) + { + } + + /// + /// Copy/Deserializing constructor. + /// + /// the box to copy from + public trafBox(Box box): + base(box) + { + Debug.Assert(box.Type == MP4BoxType.traf); + } + + + /// + /// Returns the Header box for this track. + /// + public tfhdBox Header + { + get + { + return Children.Where((box) => box is tfhdBox).Cast().Single(); + } + } + + /// + /// Reutrn the trun child box for this track. + /// + public trunBox TrackRun + { + get + { + return Children.Where((box) => box is trunBox).Cast().Single(); + } + } + + /// + /// Returns the sample dependency information for this track. + /// + public sdtpBox? DependencyInfo + { + get + { + return Children.Where((box) => box is sdtpBox).Cast().SingleOrDefault(); + } + } + + /// + /// Parses the contents of the box. + /// + protected override void ReadBody() + { + //The body of a traf box is just its children. + ReadChildren(); + } + + + #region equality methods. + /// + /// Compare the trafBox against another object for equality. + /// + /// the other object to compare with + /// + public override bool Equals(Object? other) + { + return this.Equals(other as trafBox); + } + + /// + /// returns the hashcode of the object. + /// + /// + public override int GetHashCode() + { + return base.GetHashCode(); + } + + /// + /// Compare two boxes for equality. Use base.Equals() to do the comparsion + /// + /// other box to compare against. + /// true if both boxes are equal else false. + public bool Equals(trafBox? other) + { + return base.Equals(other as Box); + } + + /// + /// Compare two trafBox objects for equality. + /// + /// left side of == + /// right side of == + /// true if the two boxes are equal else false. + public static bool operator ==(trafBox? lhs, trafBox? rhs) + { + // Check for null on left side. + if (Object.ReferenceEquals(lhs, null)) + { + if (Object.ReferenceEquals(rhs, null)) + { + // null == null = true. + return true; + } + + // Only the left side is null. + return false; + } + return lhs.Equals(rhs); + } + + /// + /// compares two entries using the Equals() method for equality. + /// + /// left side of != + /// right side of != + /// return true if two boxes are not equal else false. + public static bool operator !=(trafBox? lhs, trafBox? rhs) + { + return !(lhs == rhs); + } + + + + #endregion + } +} diff --git a/fmp4/trakBox.cs b/fmp4/trakBox.cs new file mode 100644 index 0000000..4c6b56c --- /dev/null +++ b/fmp4/trakBox.cs @@ -0,0 +1,129 @@ + +using System.Diagnostics; + +namespace AMSMigrate.Fmp4 +{ + /// + /// An object model for the 'trak' box from ISO 14496-12. + /// + public class trakBox : Box, IEquatable + { + + /// + /// Default constructor. + /// + public trakBox() : + base(MP4BoxType.trak) + { + } + + /// + /// Copy/Deserializing constructor. + /// + /// the box to copy from + public trakBox(Box box) : + base(box) + { + Debug.Assert(box.Type == MP4BoxType.trak); + } + + + /// + /// Returns the Header box for this track. + /// + public tkhdBox Header + { + get + { + return Children.Where((box) => box is tkhdBox).Cast().Single(); + } + } + + /// + /// Returns the Media box for this track. + /// + public mdiaBox Media + { + get + { + return Children.Where((box) => box is mdiaBox).Cast().Single(); + } + } + + /// + /// Parses the contents of the box. + /// + protected override void ReadBody() + { + //The body of a trak box is just its children. + ReadChildren(); + } + + + #region equality methods. + /// + /// Compare the trakBox against another object for equality. + /// + /// the other object to compare with + /// + public override bool Equals(Object? other) + { + return this.Equals(other as trakBox); + } + + /// + /// returns the hash code of the object. + /// + /// + public override int GetHashCode() + { + return base.GetHashCode(); + } + + /// + /// Compare two boxes for equality. Use base.Equals() to do the comparison + /// + /// other box to compare against. + /// true if both boxes are equal else false. + public bool Equals(trakBox? other) + { + return base.Equals(other as Box); + } + + /// + /// Compare two trakBox objects for equality. + /// + /// left side of == + /// right side of == + /// true if the two boxes are equal else false. + public static bool operator ==(trakBox? lhs, trakBox? rhs) + { + // Check for null on left side. + if (Object.ReferenceEquals(lhs, null)) + { + if (Object.ReferenceEquals(rhs, null)) + { + // null == null = true. + return true; + } + + // Only the left side is null. + return false; + } + return lhs.Equals(rhs); + } + + /// + /// compares two entries using the Equals() method for equality. + /// + /// left side of != + /// right side of != + /// return true if two boxes are not equal else false. + public static bool operator !=(trakBox? lhs, trakBox? rhs) + { + return !(lhs == rhs); + } + + #endregion + } +} diff --git a/fmp4/trexBox.cs b/fmp4/trexBox.cs new file mode 100644 index 0000000..4960603 --- /dev/null +++ b/fmp4/trexBox.cs @@ -0,0 +1,252 @@ + +using System.Diagnostics; +using System.Globalization; + +namespace AMSMigrate.Fmp4 +{ + /// + /// Represents a movie header box . ISO 14496-12 Sec:8.2.2 + /// + public class trexBox : FullBox, IEquatable + { + /// + /// Default constructor with a sequence number + /// + /// The track ID which this box refers to. + public trexBox(UInt32 trackId) : + base(version: 0, flags: 0, boxtype: MP4BoxType.trex) + { + _trackId = trackId; + } + + /// + /// Copy constructor + /// + /// the box to copy from + public trexBox(Box box) : + base(box) + { + Debug.Assert(box.Type == MP4BoxType.trex); + } + + /// + /// Get or set the track ID. + /// + public UInt32 TrackId + { + get + { + return _trackId; + } + set + { + _trackId = value; + SetDirty(); + } + } + + /// + /// The default sample description index for this track. + /// + public UInt32 DefaultSampleDescriptionIndex { get; private set; } + + /// + /// The default sample duration for this track. + /// + public UInt32 DefaultSampleDuration { get; private set; } + + /// + /// The default sample size for this track. + /// + public UInt32 DefaultSampleSize { get; private set; } + + /// + /// The default sample flags for this track. + /// + public UInt32 DefaultSampleFlags { get; private set; } + + /// + /// The track ID for this track. + /// + private UInt32 _trackId; + + + /// + /// Parse the body contents of this box. + /// + /// + protected override void ReadBody() + { + base.ReadBody(); + + long startPosition = Body!.BaseStream.Position; + + try + { + _trackId = Body.ReadUInt32(); + DefaultSampleDescriptionIndex = Body.ReadUInt32(); + DefaultSampleDuration = Body.ReadUInt32(); + DefaultSampleSize = Body.ReadUInt32(); + DefaultSampleFlags = Body.ReadUInt32(); + } + catch (EndOfStreamException ex) + { + // Reported error offset will point to start of box + throw new MP4DeserializeException(TypeDescription, -BodyPreBytes, BodyInitialOffset, + String.Format(CultureInfo.InvariantCulture, "Could not read trex fields, only {0} bytes left in reported size, expected {1}", + Body.BaseStream.Length - startPosition, ComputeLocalSize()), ex); + } + + } + + protected override void WriteToInternal(MP4Writer writer) + { + base.WriteToInternal(writer); + + writer.WriteUInt32(TrackId); + writer.WriteUInt32(DefaultSampleDescriptionIndex); + writer.WriteUInt32(DefaultSampleDuration); + writer.WriteUInt32(DefaultSampleSize); + writer.WriteUInt32(DefaultSampleFlags); + } + + /// + /// Compute the size of the box. + /// + /// size of the box itself + public override UInt64 ComputeSize() + { + return base.ComputeSize() + ComputeLocalSize(); + } + + protected static new UInt64 ComputeLocalSize() + { + return + + 4 //track id + + 4 //sample description index + + 4 //duration + + 4 //size + + 4; //flags + } + + + #region equality methods + /// + /// Compare this box to another object. + /// + /// the object to compare equality against + /// + public override bool Equals(Object? other) + { + return this.Equals(other as trexBox); + } + + /// + /// Returns the hash code of the object. + /// + /// + public override int GetHashCode() + { + return base.GetHashCode(); + } + + /// + /// Box.Equals override. This is done so that programs which attempt to test equality + /// using pointers to Box will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public override bool Equals(Box? other) + { + return this.Equals(other as trexBox); + } + + /// + /// FullBox.Equals override. This is done so that programs which attempt to test equality + /// using pointers to Box will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public override bool Equals(FullBox? other) + { + return this.Equals(other as trexBox); + } + + /// + /// Compare two trexBoxes for equality. + /// + /// other trexBox to compare against + /// + public bool Equals(trexBox? other) + { + if (!base.Equals((FullBox?)other)) + return false; + + if (TrackId != other.TrackId) + { + return false; + } + + if (DefaultSampleDescriptionIndex != other.DefaultSampleDescriptionIndex) + { + return false; + } + + if (DefaultSampleDuration != other.DefaultSampleDuration) + { + return false; + } + + if (DefaultSampleSize != other.DefaultSampleSize) + { + return false; + } + + if (DefaultSampleFlags != other.DefaultSampleFlags) + { + return false; + } + + return true; + } + + /// + /// Compare two trexBox objects for equality. + /// + /// left hand side of == + /// right hand side of == + /// + public static bool operator ==(trexBox? lhs, trexBox? rhs) + { + // Check for null on left side. + if (Object.ReferenceEquals(lhs, null)) + { + if (Object.ReferenceEquals(rhs, null)) + { + // null == null = true. + return true; + } + + // Only the left side is null. + return false; + } + return lhs.Equals(rhs); + } + + /// + /// Compare two trexBox objects for equality + /// + /// left side of != + /// right side of != + /// true if not equal else false. + public static bool operator !=(trexBox? lhs, trexBox? rhs) + { + return !(lhs == rhs); + } + + + #endregion + } +} diff --git a/fmp4/trunBox.cs b/fmp4/trunBox.cs new file mode 100644 index 0000000..14eaadd --- /dev/null +++ b/fmp4/trunBox.cs @@ -0,0 +1,539 @@ + +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Globalization; + +namespace AMSMigrate.Fmp4 +{ + /// + /// An object model for a track run box (as per ISO 14496-12) + /// + public class trunBox : FullBox, IEquatable + { + + /// + /// Default constructor. + /// + public trunBox() : + base(version:0, flags:0, boxtype:MP4BoxType.trun) + { + Size.Value = ComputeSize(); + _entries.CollectionChanged += Entries_CollectionChanged; + } + + /// + /// De-serializing/copy constructor. + /// + /// the box to construct from + public trunBox(Box box): + base(box) + { + Debug.Assert(box.Type == MP4BoxType.trun); + + _entries.CollectionChanged +=Entries_CollectionChanged; + } + + /// + /// destructor. unregister the delegate for collection changes. + /// + ~trunBox() + { + _entries.CollectionChanged -= Entries_CollectionChanged; + } + + /// + /// A delegate that is called when the entries collection is changes (entry added/removed/changed) + /// + /// + /// + private void Entries_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) + { + SetDirty(); + } + + /// + /// An enumeration to defined the various allowed flags for a tfhd box. + /// + [Flags] + public enum TrunFlags : uint + { + None = 0, + DataOffsetPresent = 0x1, + FirstSampleFlagsPresent = 0x4, + SampleDurationPresent = 0x100, + SampleSizePresent = 0x200, + SampleFlagsPresent = 0x400, + SampleCompositionOffsetPresent = 0x0800 + } + + /// + /// The offset of the data + /// + public Int32? DataOffset + { + get + { + return _dataOffset; + } + set + { + _dataOffset = value; + SetDirty(); + } + } + + private Int32? _dataOffset; + + /// + /// The flags for the first sample in the track. + /// + public UInt32? FirstSampleFlags + { + get + { + return _firstSampleFlags; + } + set + { + _firstSampleFlags = value; + SetDirty(); + } + } + + /// + /// The first sample flags.. this overrides the values + /// + private UInt32? _firstSampleFlags; + + /// + /// Calculates the size of each trun entry based on the flags. + /// + /// The flags to + /// + public static UInt32 GetEntrySize(TrunFlags flags) + { + UInt32 entrySize = 0; + + if ((flags & TrunFlags.SampleDurationPresent) == TrunFlags.SampleDurationPresent) + { + entrySize += 4; + } + + if ((flags & TrunFlags.SampleSizePresent) == TrunFlags.SampleSizePresent) + { + entrySize += 4; + } + + if ((flags & TrunFlags.SampleFlagsPresent) == TrunFlags.SampleFlagsPresent) + { + entrySize += 4; + } + + if ((flags & TrunFlags.SampleCompositionOffsetPresent) == TrunFlags.SampleCompositionOffsetPresent) + { + entrySize += 4; + } + + return entrySize; + } + + /// + /// Pases the body of box. As per 14496-12 section 8.36 + /// + protected override void ReadBody() + { + base.ReadBody(); + + TrunFlags flags = (TrunFlags)base.Flags; + Int64 startPosition = Body!.BaseStream.Position; + try + { + //Only fixed field is sample count. rest all are optional. + UInt32 sampleCount = Body.ReadUInt32(); + + if ((flags & TrunFlags.DataOffsetPresent) == TrunFlags.DataOffsetPresent) + { + _dataOffset = Body.ReadInt32(); + } + + if ((flags & TrunFlags.FirstSampleFlagsPresent) == TrunFlags.FirstSampleFlagsPresent) + { + _firstSampleFlags = Body.ReadUInt32(); + } + + UInt32 entrySize = GetEntrySize(flags); + + if (sampleCount * entrySize > Body.BaseStream.Length - Body.BaseStream.Position) + { + // Reported error offset will point to start of box + throw new MP4DeserializeException(TypeDescription, -BodyPreBytes, BodyInitialOffset, + String.Format(CultureInfo.InvariantCulture, "{0} trun entries at {1} bytes per entry exceeds the remaining box size of {2}!", + sampleCount, entrySize, Body.BaseStream.Length - Body.BaseStream.Position)); + } + + for (UInt32 i = 0; i < sampleCount; ++i) + { + trunEntry entry = new trunEntry(); + + if ((flags & TrunFlags.SampleDurationPresent) == TrunFlags.SampleDurationPresent) + { + entry.SampleDuration = Body.ReadUInt32(); + } + + if ((flags & TrunFlags.SampleSizePresent) == TrunFlags.SampleSizePresent) + { + entry.SampleSize = Body.ReadUInt32(); + } + + if ((flags & TrunFlags.SampleFlagsPresent) == TrunFlags.SampleFlagsPresent) + { + entry.SampleFlags = Body.ReadUInt32(); + } + + if ((flags & TrunFlags.SampleCompositionOffsetPresent) == TrunFlags.SampleCompositionOffsetPresent) + { + entry.SampleCompositionOffset = Body.ReadInt32(); + } + + //An entry just de-serialized is not dirty. + entry.Dirty = false; + + _entries.Add(entry); + } + } + catch (EndOfStreamException ex) + { + // Reported error offset will point to start of box + throw new MP4DeserializeException(TypeDescription, -BodyPreBytes, BodyInitialOffset, + String.Format(CultureInfo.InvariantCulture, "Could not read trun fields, only {0} bytes left in reported size, expected {1}", + Body.BaseStream.Length - startPosition, ComputeLocalSize()), ex); + } + } + + /// + /// Writes the contents of the box to the writer. + /// + /// MP4Writer to write to + protected override void WriteToInternal(MP4Writer writer) + { + if (Dirty) + { + ComputeFlags(); + } + + base.WriteToInternal(writer); + + writer.WriteInt32(_entries.Count); + + if (_dataOffset.HasValue) + { + writer.WriteInt32(_dataOffset.Value); + } + + if (_firstSampleFlags.HasValue) + { + writer.WriteUInt32(_firstSampleFlags.Value); + } + + foreach (trunEntry entry in _entries) + { + entry.WriteTo(writer); + } + } + + + /// + /// Helper method to trow an InvalidDataException if the trun entries are not consistent. + /// + /// The name of field in trun entry that is inconsitent + /// first trun entry + /// current trun entry + /// index of the current entry + private static void ThrowValidationException(String field, bool first, bool current, int index) + { + throw new InvalidDataException( + String.Format(CultureInfo.InvariantCulture, "Field:{0} is inconsistent for trun entries. Entry[0]: {1}, Entry[{3}]: {2}", + field, + first, + current, + index)); + } + + /// + /// Compute the flags to be set. + /// Also validate the entries to make sure all entries have exact same fields. + /// + private void ComputeFlags() + { + TrunFlags flags = TrunFlags.None; + + if (_dataOffset.HasValue) + { + flags |= TrunFlags.DataOffsetPresent; + } + + if (_firstSampleFlags.HasValue) + { + flags |= TrunFlags.FirstSampleFlagsPresent; + } + + if (_entries.Count > 0) + { + trunEntry firstEntry = _entries[0]; + + if (firstEntry.SampleDuration.HasValue) + { + flags |= TrunFlags.SampleDurationPresent; + } + + if (firstEntry.SampleSize.HasValue) + { + flags |= TrunFlags.SampleSizePresent; + } + if (firstEntry.SampleFlags.HasValue) + { + flags |= TrunFlags.SampleFlagsPresent; + } + if (firstEntry.SampleCompositionOffset.HasValue) + { + flags |= TrunFlags.SampleCompositionOffsetPresent; + } + + for (int i = 1; i < _entries.Count; ++i) + { + trunEntry entry = _entries[i]; + + if (entry.SampleDuration.HasValue != firstEntry.SampleDuration.HasValue) + { + ThrowValidationException("SampleDuration", firstEntry.SampleDuration.HasValue, entry.SampleDuration.HasValue, i); + } + + if (entry.SampleSize.HasValue != firstEntry.SampleSize.HasValue) + { + ThrowValidationException("SampleSize", firstEntry.SampleSize.HasValue, entry.SampleSize.HasValue, i); + } + + if (entry.SampleFlags.HasValue != firstEntry.SampleFlags.HasValue) + { + ThrowValidationException("SampleFlags", firstEntry.SampleFlags.HasValue, entry.SampleFlags.HasValue, i); + } + + if (entry.SampleCompositionOffset.HasValue != firstEntry.SampleCompositionOffset.HasValue) + { + ThrowValidationException("SampleCompositionOffset", firstEntry.SampleCompositionOffset.HasValue, entry.SampleCompositionOffset.HasValue, i); + } + } + } + + Flags = (UInt32)flags; + } + + /// + /// Calculate the current size of this box. + /// + /// The current size of this box, if it were to be written to disk now. + public override UInt64 ComputeSize() + { + return base.ComputeSize() + ComputeLocalSize(); + } + + /// + /// Calculates the current size of just this class (base classes excluded). This is called by + /// ComputeSize(). + /// + /// The current size of just the fields from this box. + protected new UInt64 ComputeLocalSize() + { + UInt64 thisSize = 4; //for the sample count. rest all are optional. + UInt64 entrySize = 0; + + if(_dataOffset.HasValue) + { + thisSize += 4; + } + + if (_firstSampleFlags.HasValue) + { + thisSize += 4; + } + + if (_entries.Count > 0) + { + trunEntry firstEntry = _entries[0]; + + if (firstEntry.SampleDuration.HasValue) + { + entrySize += 4; + } + + if (firstEntry.SampleSize.HasValue) + { + entrySize += 4; + } + + if (firstEntry.SampleFlags.HasValue) + { + entrySize += 4; + } + + if (firstEntry.SampleCompositionOffset.HasValue) + { + entrySize += 4; + } + + } + + thisSize += (entrySize * (UInt64)_entries.Count); + return thisSize; + } + + /// + /// Indicate if the box is dirty or not. + /// + public override bool Dirty + { + get + { + if (base.Dirty) + { + return true; + } + + foreach (trunEntry entry in _entries) + { + if (entry.Dirty) + { + return true; + } + } + + return false; + } + set + { + base.Dirty = value; + foreach (trunEntry entry in _entries) + { + entry.Dirty = value; + } + } + } + + /// + /// A collection of track run entries. + /// + public IList Entries + { + get + { + return _entries; + } + } + + private ObservableCollection _entries = new ObservableCollection(); + + #region equality methods. + + /// + /// Compare this box to another object. + /// + /// the object to compare against. + /// true if both are equal else false. + public override bool Equals(object? obj) + { + return this.Equals(obj as trunBox); + } + + /// + /// returns the hashcode of the object. + /// + /// + public override int GetHashCode() + { + return base.GetHashCode(); + } + + /// + /// Box.Equals override. This is done so that programs which attempt to test equality + /// using pointers to Box will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public override bool Equals(Box? other) + { + return this.Equals(other as trunBox); + } + + /// + /// FullBox.Equals override. This is done so that programs which attempt to test equality + /// using pointers to Box will can enjoy results from the fully derived + /// equality implementation. + /// + /// The box to test equality against. + /// True if the this and the given box are equal. + public override bool Equals(FullBox? other) + { + return this.Equals(other as trunBox); + } + + /// + /// compare this object against another trunBox object. + /// + /// other trunBox to compare against. + /// true if equal else false. + public bool Equals(trunBox? other) + { + if (!base.Equals((FullBox?)other)) + { + return false; + } + if (_dataOffset != other._dataOffset) + { + return false; + } + if (_firstSampleFlags != other._firstSampleFlags) + { + return false; + } + + return _entries.SequenceEqual(other._entries); + } + + /// + /// Compare two trunBox objects for equality using Equals() method. + /// + /// left side of == + /// right side of == + /// true if the two boxes are equal else false. + public static bool operator ==(trunBox? lhs, trunBox? rhs) + { + // Check for null on left side. + if (Object.ReferenceEquals(lhs, null)) + { + if (Object.ReferenceEquals(rhs, null)) + { + // null == null = true. + return true; + } + + // Only the left side is null. + return false; + } + return lhs.Equals(rhs); + } + + /// + /// compares two entries using the Equals() method for equality. + /// + /// left side of != + /// right side of != + /// return true if two boxes are not equal else false. + public static bool operator !=(trunBox? lhs, trunBox? rhs) + { + return !(lhs ==rhs); + } + + #endregion + } +} diff --git a/fmp4/trunEntry.cs b/fmp4/trunEntry.cs new file mode 100644 index 0000000..1b3b160 --- /dev/null +++ b/fmp4/trunEntry.cs @@ -0,0 +1,268 @@ + +namespace AMSMigrate.Fmp4 +{ + /// + /// This is the object model presentation of each entry in the 'trun' box + /// as defined in ISO 14496-12. + /// + public class trunEntry : IEquatable + { + /// + /// Default constructor. + /// + public trunEntry() + { + Dirty = true; + } + + /// + /// Deep Copy constructor (all members are value types, so there is no shallow copy). + /// + /// The instance to copy from. + public trunEntry(trunEntry other) + { + _sampleDuration = other._sampleDuration; + _sampleSize = other._sampleSize; + _sampleFlags = other._sampleFlags; + _sampleCompositionOffset = other._sampleCompositionOffset; + Dirty = true; + } + + /// + /// Optional sample duration of this entry. + /// + public UInt32? SampleDuration + { + get + { + return _sampleDuration; + } + set + { + _sampleDuration = value; + Dirty = true; + } + } + + /// + /// backing field for SampleDuration property. + /// + private UInt32? _sampleDuration; + + /// + /// optional sample size of this entry. + /// + public UInt32? SampleSize + { + get + { + return _sampleSize; + } + set + { + _sampleSize = value; + Dirty = true; + } + } + + /// + /// backing field for SampleSize property. + /// + private UInt32? _sampleSize; + + /// + /// Optional sample flags for this entry. + /// + public UInt32? SampleFlags + { + get + { + return _sampleFlags; + } + set + { + _sampleFlags = value; + Dirty = true; + } + } + + /// + /// backing field for SampleFlags property. + /// + private UInt32? _sampleFlags; + + + /// + /// optional sample composition offset for this entry. + /// Note that spec says it is UInt32 if version = 0 and Int32 if version = 1 or higher. + /// but we have content that uses it as int32 with version = 0. and offset > Int32.MaxValue is not + /// possible anyways. so always keep it Int32. + /// + public Int32? SampleCompositionOffset + { + get + { + return _sampleCompositionOffset; + } + set + { + _sampleCompositionOffset = value; + Dirty = true; + } + } + + /// + /// backing field for SampleCompositionOffset property. + /// + private Int32? _sampleCompositionOffset; + + /// + /// True if this entry matches the on-disk representation, either because we deserialized and + /// no further changes were made, or because we just saved to disk and no further changes were made. + /// + public bool Dirty { get; set; } + + + /// + /// Serialize the entry to the mp4 writer. + /// + /// the MP4Writer to write to + public void WriteTo(MP4Writer writer) + { + if (null == writer) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (_sampleDuration.HasValue) + { + writer.WriteUInt32(_sampleDuration.Value); + } + + if (_sampleSize.HasValue) + { + writer.WriteUInt32(_sampleSize.Value); + } + + if (_sampleFlags.HasValue) + { + writer.WriteUInt32(_sampleFlags.Value); + } + + if (_sampleCompositionOffset.HasValue) + { + writer.WriteInt32(_sampleCompositionOffset.Value); + } + + Dirty = false; + } + + #region Equality Methods + + //===================================================================== + // Equality Methods + // + //===================================================================== + + /// + /// Object.Equals override. + /// + /// The object to test equality against. + /// True if the this and the given object are equal. + public override bool Equals(Object? obj) + { + return this.Equals(obj as trunEntry); + } + + /// + /// Implements IEquatable(trunEntry). This function is virtual and it is expected that + /// derived classes will override it, so that programs which attempt to test equality + /// using pointers to base classes will can enjoy results from the fully derived + /// equality implementation. + /// + /// The entry to test equality against. + /// True if the this and the given entry are equal. + public virtual bool Equals(trunEntry? other) + { + // If parameter is null, return false. + if (Object.ReferenceEquals(other, null)) + { + return false; + } + + // Optimization for a common success case. + if (Object.ReferenceEquals(this, other)) + { + return true; + } + + // If run-time types are not exactly the same, return false. + if (this.GetType() != other.GetType()) + return false; + + // Compare fields + if (_sampleSize != other._sampleSize) + return false; + + if (_sampleDuration != other._sampleDuration) + return false; + + if (_sampleCompositionOffset != other._sampleCompositionOffset) + return false; + + if (_sampleFlags != other._sampleFlags) + return false; + + // If we reach this point, the fields all match + return true; + } + + /// + /// Object.GetHashCode override. This must be done as a consequence of overridding + /// Object.Equals. + /// + /// Hash code which will be match the hash code of an object which is equal. + public override int GetHashCode() + { + return GetType().GetHashCode(); + } + + /// + /// Override == operation (as recommended by MSDN). + /// + /// The entry on the left-hand side of the ==. + /// The entry on the right-hand side of the ==. + /// True if the two entries are equal. + public static bool operator ==(trunEntry? lhs, trunEntry? rhs) + { + // Check for null on left side. + if (Object.ReferenceEquals(lhs, null)) + { + if (Object.ReferenceEquals(rhs, null)) + { + // null == null = true. + return true; + } + + // Only the left side is null. + return false; + } + // Equals handles case of null on right side. + return lhs.Equals(rhs); + } + + /// + /// Override != operation (as recommended by MSDN). + /// + /// The entry on the left-hand side of the !=. + /// The entry on the right-hand side of the !=. + /// True if the two entries are equal. + public static bool operator !=(trunEntry? lhs, trunEntry? rhs) + { + return !(lhs == rhs); + } + + #endregion + + } +}