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
+
+ }
+}