mkvmuxer: add support for WebVTT chapters

Change-Id: I469ce3bd79a9b50b82e00ac8c63fc3d1db220887
This commit is contained in:
Matthew Heaney 2012-10-22 13:05:11 -07:00
parent 25ee621061
commit 2c5836837e
3 changed files with 600 additions and 56 deletions

View File

@ -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<uint32>(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<unsigned int>(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;

View File

@ -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_;

View File

@ -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
#endif // WEBMIDS_HPP