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