diff --git a/mkvmuxer.cpp b/mkvmuxer.cpp index 27ea7de..c77a0d3 100644 --- a/mkvmuxer.cpp +++ b/mkvmuxer.cpp @@ -21,6 +21,36 @@ namespace mkvmuxer { +namespace { +// Deallocate the string designated by |dst|, and then copy the |src| +// string to |dst|. The caller owns both the |src| string and the +// |dst| copy (hence the caller is responsible for eventually +// deallocating the strings, either directly, or indirectly via +// StrCpy). Returns true if the source string was successfully copied +// to the destination. +bool StrCpy(const char* src, char** dst_ptr) { + if (dst_ptr == NULL) + return false; + + char*& dst = *dst_ptr; + + delete [] dst; + dst = NULL; + + if (src == NULL) + return true; + + const size_t size = strlen(src) + 1; + + dst = new (std::nothrow) char[size]; // NOLINT + if (dst == NULL) + return false; + + strcpy(dst, src); // NOLINT + return true; +} +} // namespace + /////////////////////////////////////////////////////////////// // // IMkvWriter Class @@ -427,14 +457,14 @@ uint64 ContentEncoding::EncryptionSize() const { // // Track Class -Track::Track() +Track::Track(unsigned int* seed) : codec_id_(NULL), codec_private_(NULL), language_(NULL), name_(NULL), number_(0), type_(0), - uid_(MakeUID()), + uid_(MakeUID(seed)), codec_private_length_(0), content_encoding_entries_(NULL), content_encoding_entries_size_(0) { @@ -679,33 +709,13 @@ void Track::set_name(const char* name) { } } -bool Track::is_seeded_ = false; - -uint64 Track::MakeUID() { - if (!is_seeded_) { - srand(static_cast(time(NULL))); - is_seeded_ = true; - } - - uint64 track_uid = 0; - for (int32 i = 0; i < 7; ++i) { // avoid problems with 8-byte values - track_uid <<= 8; - - const int32 nn = rand(); - const int32 n = 0xFF & (nn >> 4); // throw away low-order bits - - track_uid |= n; - } - - return track_uid; -} - /////////////////////////////////////////////////////////////// // // VideoTrack Class -VideoTrack::VideoTrack() - : display_height_(0), +VideoTrack::VideoTrack(unsigned int* seed) + : Track(seed), + display_height_(0), display_width_(0), frame_rate_(0.0), height_(0), @@ -796,8 +806,9 @@ uint64 VideoTrack::VideoPayloadSize() const { // // AudioTrack Class -AudioTrack::AudioTrack() - : bit_depth_(0), +AudioTrack::AudioTrack(unsigned int* seed) + : Track(seed), + bit_depth_(0), channels_(1), sample_rate_(0.0) { } @@ -1008,7 +1019,344 @@ bool Tracks::Write(IMkvWriter* writer) const { /////////////////////////////////////////////////////////////// // -// Cluster Class +// Chapter Class + +bool Chapter::set_id(const char* id) { + return StrCpy(id, &id_); +} + +void Chapter::set_time(const Segment& segment, + uint64 start_ns, + uint64 end_ns) { + const SegmentInfo* const info = segment.GetSegmentInfo(); + const uint64 timecode_scale = info->timecode_scale(); + start_timecode_ = start_ns / timecode_scale; + end_timecode_ = end_ns / timecode_scale; +} + +bool Chapter::add_string(const char* title, + const char* language, + const char* country) { + if (!ExpandDisplaysArray()) + return false; + + Display& d = displays_[displays_count_++]; + d.Init(); + + if (!d.set_title(title)) + return false; + + if (!d.set_language(language)) + return false; + + if (!d.set_country(country)) + return false; + + return true; +} + +Chapter::Chapter() { + // This ctor only constructs the object. Proper initialization is + // done in Init() (called in Chapters::AddChapter()). The only + // reason we bother implementing this ctor is because we had to + // declare it as private (along with the dtor), in order to prevent + // clients from creating Chapter instances (a privelege we grant + // only to the Chapters class). Doing no initialization here also + // means that creating arrays of chapter objects is more efficient, + // because we only initialize each new chapter object as it becomes + // active on the array. +} + +Chapter::~Chapter() { +} + +void Chapter::Init(unsigned int* seed) { + id_ = NULL; + displays_ = NULL; + displays_size_ = 0; + displays_count_ = 0; + uid_ = MakeUID(seed); +} + +void Chapter::ShallowCopy(Chapter* dst) const { + dst->id_ = id_; + dst->start_timecode_ = start_timecode_; + dst->end_timecode_ = end_timecode_; + dst->uid_ = uid_; + dst->displays_ = displays_; + dst->displays_size_ = displays_size_; + dst->displays_count_ = displays_count_; +} + +void Chapter::Clear() { + StrCpy(NULL, &id_); + + while (displays_count_ < 0) { + Display& d = displays_[--displays_count_]; + d.Clear(); + } + + delete [] displays_; + displays_ = NULL; + + displays_size_ = 0; +} + +bool Chapter::ExpandDisplaysArray() { + if (displays_size_ > displays_count_) + return true; // nothing to do yet + + const int size = (displays_size_ == 0) ? 1 : 2 * displays_size_; + + Display* const displays = new (std::nothrow) Display[size]; // NOLINT + if (displays == NULL) + return false; + + for (int idx = 0; idx < displays_count_; ++idx) { + displays[idx] = displays_[idx]; // shallow copy + } + + delete [] displays_; + + displays_ = displays; + displays_size_ = size; + + return true; +} + +uint64 Chapter::WriteAtom(IMkvWriter* writer) const { + uint64 payload_size = + // TODO(matthewjheaney): resolve ID issue + EbmlElementSize(kMkvChapterUID, uid_) + + EbmlElementSize(kMkvChapterTimeStart, start_timecode_) + + EbmlElementSize(kMkvChapterTimeEnd, end_timecode_); + + for (int idx = 0; idx < displays_count_; ++idx) { + const Display& d = displays_[idx]; + payload_size += d.WriteDisplay(NULL); + } + + const uint64 atom_size = + EbmlMasterElementSize(kMkvChapterAtom, payload_size) + + payload_size; + + if (writer == NULL) + return atom_size; + + const int64 start = writer->Position(); + + if (!WriteEbmlMasterElement(writer, kMkvChapterAtom, payload_size)) + return 0; + + if (!WriteEbmlElement(writer, kMkvChapterUID, uid_)) + return 0; + + if (!WriteEbmlElement(writer, kMkvChapterTimeStart, start_timecode_)) + return 0; + + if (!WriteEbmlElement(writer, kMkvChapterTimeEnd, end_timecode_)) + return 0; + + for (int idx = 0; idx < displays_count_; ++idx) { + const Display& d = displays_[idx]; + + if (!d.WriteDisplay(writer)) + return 0; + } + + const int64 stop = writer->Position(); + + if (stop >= start && uint64(stop - start) != atom_size) + return 0; + + return atom_size; +} + +void Chapter::Display::Init() { + title_ = NULL; + language_ = NULL; + country_ = NULL; +} + +void Chapter::Display::Clear() { + StrCpy(NULL, &title_); + StrCpy(NULL, &language_); + StrCpy(NULL, &country_); +} + +bool Chapter::Display::set_title(const char* title) { + return StrCpy(title, &title_); +} + +bool Chapter::Display::set_language(const char* language) { + return StrCpy(language, &language_); +} + +bool Chapter::Display::set_country(const char* country) { + return StrCpy(country, &country_); +} + +uint64 Chapter::Display::WriteDisplay(IMkvWriter* writer) const { + uint64 payload_size = EbmlElementSize(kMkvChapString, title_); + + if (language_) + payload_size += EbmlElementSize(kMkvChapLanguage, language_); + + if (country_) + payload_size += EbmlElementSize(kMkvChapCountry, country_); + + const uint64 display_size = + EbmlMasterElementSize(kMkvChapterDisplay, payload_size) + + payload_size; + + if (writer == NULL) + return display_size; + + const int64 start = writer->Position(); + + if (!WriteEbmlMasterElement(writer, kMkvChapterDisplay, payload_size)) + return 0; + + if (!WriteEbmlElement(writer, kMkvChapString, title_)) + return 0; + + if (language_) { + if (!WriteEbmlElement(writer, kMkvChapLanguage, language_)) + return 0; + } + + if (country_) { + if (!WriteEbmlElement(writer, kMkvChapCountry, country_)) + return 0; + } + + const int64 stop = writer->Position(); + + if (stop >= start && uint64(stop - start) != display_size) + return 0; + + return display_size; +} + +/////////////////////////////////////////////////////////////// +// +// Chapters Class + +Chapters::Chapters() + : chapters_size_(0), + chapters_count_(0), + chapters_(NULL) { +} + +Chapters::~Chapters() { + while (chapters_count_ > 0) { + Chapter& chapter = chapters_[--chapters_count_]; + chapter.Clear(); + } + + delete [] chapters_; + chapters_ = NULL; +} + +int Chapters::Count() const { + return chapters_count_; +} + +Chapter* Chapters::AddChapter(unsigned int* seed) { + if (!ExpandChaptersArray()) + return NULL; + + Chapter& chapter = chapters_[chapters_count_++]; + chapter.Init(seed); + + return &chapter; +} + +bool Chapters::Write(IMkvWriter* writer) const { + if (writer == NULL) + return false; + + const uint64 payload_size = WriteEdition(NULL); // return size only + + if (!WriteEbmlMasterElement(writer, kMkvChapters, payload_size)) + return false; + + const int64 start = writer->Position(); + + if (WriteEdition(writer) == 0) // error + return false; + + const int64 stop = writer->Position(); + + if (stop >= start && uint64(stop - start) != payload_size) + return false; + + return true; +} + +bool Chapters::ExpandChaptersArray() { + if (chapters_size_ > chapters_count_) + return true; // nothing to do yet + + const int size = (chapters_size_ == 0) ? 1 : 2 * chapters_size_; + + Chapter* const chapters = new (std::nothrow) Chapter[size]; // NOLINT + if (chapters == NULL) + return false; + + for (int idx = 0; idx < chapters_count_; ++idx) { + const Chapter& src = chapters_[idx]; + Chapter* const dst = chapters + idx; + src.ShallowCopy(dst); + } + + delete [] chapters_; + + chapters_ = chapters; + chapters_size_ = size; + + return true; +} + +uint64 Chapters::WriteEdition(IMkvWriter* writer) const { + uint64 payload_size = 0; + + for (int idx = 0; idx < chapters_count_; ++idx) { + const Chapter& chapter = chapters_[idx]; + payload_size += chapter.WriteAtom(NULL); + } + + const uint64 edition_size = + EbmlMasterElementSize(kMkvEditionEntry, payload_size) + + payload_size; + + if (writer == NULL) // return size only + return edition_size; + + const int64 start = writer->Position(); + + if (!WriteEbmlMasterElement(writer, kMkvEditionEntry, payload_size)) + return 0; // error + + for (int idx = 0; idx < chapters_count_; ++idx) { + const Chapter& chapter = chapters_[idx]; + + const uint64 chapter_size = chapter.WriteAtom(writer); + if (chapter_size == 0) // error + return 0; + } + + const int64 stop = writer->Position(); + + if (stop >= start && uint64(stop - start) != edition_size) + return 0; + + return edition_size; +} + +/////////////////////////////////////////////////////////////// +// +// Cluster class Cluster::Cluster(uint64 timecode, int64 cues_pos) : blocks_added_(0), @@ -1477,6 +1825,8 @@ Segment::Segment() writer_cluster_(NULL), writer_cues_(NULL), writer_header_(NULL) { + const time_t curr_time = time(NULL); + seed_ = static_cast(curr_time); } Segment::~Segment() { @@ -1608,7 +1958,7 @@ bool Segment::Finalize() { } Track* Segment::AddTrack(int32 number) { - Track* const track = new (std::nothrow) Track; // NOLINT + Track* const track = new (std::nothrow) Track(&seed_); // NOLINT if (!track) return NULL; @@ -1621,20 +1971,24 @@ Track* Segment::AddTrack(int32 number) { return track; } +Chapter* Segment::AddChapter() { + return chapters_.AddChapter(&seed_); +} + uint64 Segment::AddVideoTrack(int32 width, int32 height, int32 number) { - VideoTrack* const vid_track = new (std::nothrow) VideoTrack(); // NOLINT - if (!vid_track) + VideoTrack* const track = new (std::nothrow) VideoTrack(&seed_); // NOLINT + if (!track) return 0; - vid_track->set_type(Tracks::kVideo); - vid_track->set_codec_id(Tracks::kVp8CodecId); - vid_track->set_width(width); - vid_track->set_height(height); + track->set_type(Tracks::kVideo); + track->set_codec_id(Tracks::kVp8CodecId); + track->set_width(width); + track->set_height(height); - tracks_.AddTrack(vid_track, number); + tracks_.AddTrack(track, number); has_video_ = true; - return vid_track->number(); + return track->number(); } bool Segment::AddCuePoint(uint64 timestamp, uint64 track) { @@ -1663,18 +2017,18 @@ bool Segment::AddCuePoint(uint64 timestamp, uint64 track) { uint64 Segment::AddAudioTrack(int32 sample_rate, int32 channels, int32 number) { - AudioTrack* const aud_track = new (std::nothrow) AudioTrack(); // NOLINT - if (!aud_track) + AudioTrack* const track = new (std::nothrow) AudioTrack(&seed_); // NOLINT + if (!track) return 0; - aud_track->set_type(Tracks::kAudio); - aud_track->set_codec_id(Tracks::kVorbisCodecId); - aud_track->set_sample_rate(sample_rate); - aud_track->set_channels(channels); + track->set_type(Tracks::kAudio); + track->set_codec_id(Tracks::kVorbisCodecId); + track->set_sample_rate(sample_rate); + track->set_channels(channels); - tracks_.AddTrack(aud_track, number); + tracks_.AddTrack(track, number); - return aud_track->number(); + return track->number(); } bool Segment::AddFrame(const uint8* frame, @@ -1920,6 +2274,13 @@ bool Segment::WriteSegmentHeader() { if (!tracks_.Write(writer_header_)) return false; + if (chapters_.Count() > 0) { + if (!seek_head_.AddSeekEntry(kMkvChapters, MaxOffset())) + return false; + if (!chapters_.Write(writer_header_)) + return false; + } + if (chunking_ && (mode_ == kLive || !writer_header_->Seekable())) { if (!chunk_writer_header_) return false; diff --git a/mkvmuxer.hpp b/mkvmuxer.hpp index 537361e..70c8a28 100644 --- a/mkvmuxer.hpp +++ b/mkvmuxer.hpp @@ -17,6 +17,7 @@ namespace mkvmuxer { class MkvWriter; +class Segment; /////////////////////////////////////////////////////////////// // Interface used by the mkvmuxer to write out the Mkv data. @@ -266,7 +267,8 @@ class ContentEncoding { // Track element. class Track { public: - Track(); + // The |seed| parameter is used to synthesize a UID for the track. + explicit Track(unsigned int* seed); virtual ~Track(); // Adds a ContentEncoding element to the Track. Returns true on success. @@ -308,9 +310,6 @@ class Track { } private: - // Returns a random number to be used for the Track UID. - static uint64 MakeUID(); - // Track element names char* codec_id_; uint8* codec_private_; @@ -329,9 +328,6 @@ class Track { // Number of ContentEncoding elements added. uint32 content_encoding_entries_size_; - // Flag telling if the rand call was seeded. - static bool is_seeded_; - LIBWEBM_DISALLOW_COPY_AND_ASSIGN(Track); }; @@ -348,7 +344,8 @@ class VideoTrack : public Track { kSideBySideRightIsFirst = 11 }; - VideoTrack(); + // The |seed| parameter is used to synthesize a UID for the track. + explicit VideoTrack(unsigned int* seed); virtual ~VideoTrack(); // Returns the size in bytes for the payload of the Track element plus the @@ -392,7 +389,8 @@ class VideoTrack : public Track { // Track that has audio specific elements. class AudioTrack : public Track { public: - AudioTrack(); + // The |seed| parameter is used to synthesize a UID for the track. + explicit AudioTrack(unsigned int* seed); virtual ~AudioTrack(); // Returns the size in bytes for the payload of the Track element plus the @@ -468,6 +466,170 @@ class Tracks { LIBWEBM_DISALLOW_COPY_AND_ASSIGN(Tracks); }; +/////////////////////////////////////////////////////////////// +// Chapter element +// +class Chapter { + public: + // Set the identifier for this chapter. (This corresponds to the + // Cue Identifier line in WebVTT.) + // TODO(matthewjheaney): the actual serialization of this item in + // MKV is pending. + bool set_id(const char* id); + + // Converts the nanosecond start and stop times of this chapter to + // their corresponding timecode values, and stores them that way. + void set_time(const Segment& segment, + uint64 start_time_ns, + uint64 end_time_ns); + + // Add a title string to this chapter, per the semantics described + // here: + // http://www.matroska.org/technical/specs/index.html + // + // The title ("chapter string") is a UTF-8 string. + // + // The language has ISO 639-2 representation, described here: + // http://www.loc.gov/standards/iso639-2/englangn.html + // http://www.loc.gov/standards/iso639-2/php/English_list.php + // If you specify NULL as the language value, this implies + // English ("eng"). + // + // The country value corresponds to the codes listed here: + // http://www.iana.org/domains/root/db/ + // + // The function returns false if the string could not be allocated. + bool add_string(const char* title, + const char* language, + const char* country); + + private: + friend class Chapters; + + // For storage of chapter titles that differ by language. + class Display { + public: + // Establish representation invariant for new Display object. + void Init(); + + // Reclaim resources, in anticipation of destruction. + void Clear(); + + // Copies the title to the |title_| member. Returns false on + // error. + bool set_title(const char* title); + + // Copies the language to the |language_| member. Returns false + // on error. + bool set_language(const char* language); + + // Copies the country to the |country_| member. Returns false on + // error. + bool set_country(const char* country); + + // If |writer| is non-NULL, serialize the Display sub-element of + // the Atom into the stream. Returns the Display element size on + // success, 0 if error. + uint64 WriteDisplay(IMkvWriter* writer) const; + + private: + char* title_; + char* language_; + char* country_; + }; + + Chapter(); + ~Chapter(); + + // Establish the representation invariant for a newly-created + // Chapter object. The |seed| parameter is used to create the UID + // for this chapter atom. + void Init(unsigned int* seed); + + // Copies this Chapter object to a different one. This is used when + // expanding a plain array of Chapter objects (see Chapters). + void ShallowCopy(Chapter* dst) const; + + // Reclaim resources used by this Chapter object, pending its + // destruction. + void Clear(); + + // If there is no storage remaining on the |displays_| array for a + // new display object, creates a new, longer array and copies the + // existing Display objects to the new array. Returns false if the + // array cannot be expanded. + bool ExpandDisplaysArray(); + + // If |writer| is non-NULL, serialize the Atom sub-element into the + // stream. Returns the total size of the element on success, 0 if + // error. + uint64 WriteAtom(IMkvWriter* writer) const; + + // The string identifier for this chapter (corresponds to WebVTT cue + // identifier). + char* id_; + + // Start timecode of the chapter. + uint64 start_timecode_; + + // Stop timecode of the chapter. + uint64 end_timecode_; + + // The binary identifier for this chapter. + uint64 uid_; + + // The Atom element can contain multiple Display sub-elements, as + // the same logical title can be rendered in different languages. + Display* displays_; + + // The physical length (total size) of the |displays_| array. + int displays_size_; + + // The logical length (number of active elements) on the |displays_| + // array. + int displays_count_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(Chapter); +}; + +/////////////////////////////////////////////////////////////// +// Chapters element +// +class Chapters { + public: + Chapters(); + ~Chapters(); + + Chapter* AddChapter(unsigned int* seed); + + // Returns the number of chapters that have been added. + int Count() const; + + // Output the Chapters element to the writer. Returns true on success. + bool Write(IMkvWriter* writer) const; + + private: + // Expands the chapters_ array if there is not enough space to contain + // another chapter object. Returns true on success. + bool ExpandChaptersArray(); + + // If |writer| is non-NULL, serialize the Edition sub-element of the + // Chapters element into the stream. Returns the Edition element + // size on success, 0 if error. + uint64 WriteEdition(IMkvWriter* writer) const; + + // Total length of the chapters_ array. + int chapters_size_; + + // Number of active chapters on the chapters_ array. + int chapters_count_; + + // Array for storage of chapter objects. + Chapter* chapters_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(Chapters); +}; + /////////////////////////////////////////////////////////////// // Cluster element // @@ -606,7 +768,7 @@ class SeekHead { private: // We are going to put a cap on the number of Seek Entries. - const static int32 kSeekEntryCount = 4; + const static int32 kSeekEntryCount = 5; // Returns the maximum size in bytes of one seek entry. uint64 MaxEntrySize() const; @@ -701,6 +863,11 @@ class Segment { // the track number. uint64 AddAudioTrack(int32 sample_rate, int32 channels, int32 number); + // Adds an empty chapter to the chapters of this segment. Returns + // non-NULL on success. After adding the chapter, the caller should + // populate its fields via the Chapter member functions. + Chapter* AddChapter(); + // Adds a cue point to the Cues element. |timestamp| is the time in // nanoseconds of the cue's time. |track| is the Track of the Cue. Returns // true on success. @@ -760,6 +927,7 @@ class Segment { Cues* GetCues() { return &cues_; } // Returns the Segment Information object. + const SegmentInfo* GetSegmentInfo() const { return &segment_info_; } SegmentInfo* GetSegmentInfo() { return &segment_info_; } // Search the Tracks and return the track that matches |track_number|. @@ -846,11 +1014,15 @@ class Segment { // was necessary but creation was not successful. bool DoNewClusterProcessing(uint64 track_num, uint64 timestamp_ns, bool key); + // Seeds the random number generator used to make UIDs. + unsigned int seed_; + // WebM elements Cues cues_; SeekHead seek_head_; SegmentInfo segment_info_; Tracks tracks_; + Chapters chapters_; // Number of chunks written. int chunk_count_; diff --git a/webmids.hpp b/webmids.hpp index 07827c4..453fa16 100644 --- a/webmids.hpp +++ b/webmids.hpp @@ -113,8 +113,19 @@ enum MkvId { kMkvCueTrack = 0xF7, kMkvCueClusterPosition = 0xF1, kMkvCueBlockNumber = 0x5378, + //Chapters + kMkvChapters = 0x1043A770, + kMkvEditionEntry = 0x45B9, + kMkvChapterAtom = 0xB6, + kMkvChapterUID = 0x73C4, + kMkvChapterTimeStart = 0x91, + kMkvChapterTimeEnd = 0x92, + kMkvChapterDisplay = 0x80, + kMkvChapString = 0x85, + kMkvChapLanguage = 0x437C, + kMkvChapCountry = 0x437E }; } // end namespace mkvmuxer -#endif // WEBMIDS_HPP \ No newline at end of file +#endif // WEBMIDS_HPP