diff --git a/packager/media/base/text_track_config.cc b/packager/media/base/text_track_config.cc new file mode 100644 index 0000000000..0d4b11f6dd --- /dev/null +++ b/packager/media/base/text_track_config.cc @@ -0,0 +1,30 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "media/base/text_track_config.h" + +namespace media { + +TextTrackConfig::TextTrackConfig() + : kind_(kTextNone) { +} + +TextTrackConfig::TextTrackConfig(TextKind kind, + const std::string& label, + const std::string& language, + const std::string& id) + : kind_(kind), + label_(label), + language_(language), + id_(id) { +} + +bool TextTrackConfig::Matches(const TextTrackConfig& config) const { + return config.kind() == kind_ && + config.label() == label_ && + config.language() == language_ && + config.id() == id_; +} + +} // namespace media diff --git a/packager/media/base/text_track_config.h b/packager/media/base/text_track_config.h new file mode 100644 index 0000000000..58efba4b03 --- /dev/null +++ b/packager/media/base/text_track_config.h @@ -0,0 +1,48 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef MEDIA_BASE_TEXT_TRACK_CONFIG_H_ +#define MEDIA_BASE_TEXT_TRACK_CONFIG_H_ + +#include + +#include "media/base/media_export.h" + +namespace media { + +// Specifies the varieties of text tracks. +enum TextKind { + kTextSubtitles, + kTextCaptions, + kTextDescriptions, + kTextMetadata, + kTextNone +}; + +class MEDIA_EXPORT TextTrackConfig { + public: + TextTrackConfig(); + TextTrackConfig(TextKind kind, + const std::string& label, + const std::string& language, + const std::string& id); + + // Returns true if all fields in |config| match this config. + bool Matches(const TextTrackConfig& config) const; + + TextKind kind() const { return kind_; } + const std::string& label() const { return label_; } + const std::string& language() const { return language_; } + const std::string& id() const { return id_; } + + private: + TextKind kind_; + std::string label_; + std::string language_; + std::string id_; +}; + +} // namespace media + +#endif // MEDIA_BASE_TEXT_TRACK_H_ diff --git a/packager/media/filters/webvtt_util.h b/packager/media/filters/webvtt_util.h new file mode 100644 index 0000000000..b71b66f720 --- /dev/null +++ b/packager/media/filters/webvtt_util.h @@ -0,0 +1,30 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef MEDIA_FILTERS_WEBVTT_UTIL_H_ +#define MEDIA_FILTERS_WEBVTT_UTIL_H_ + +#include + +namespace media { + +// Utility function to create side data item for decoder buffer. +template +void MakeSideData(T id_begin, T id_end, + T settings_begin, T settings_end, + std::vector* side_data) { + // The DecoderBuffer only supports a single side data item. In the case of + // a WebVTT cue, we can have potentially two side data items. In order to + // avoid disrupting DecoderBuffer any more than we need to, we copy both + // side data items onto a single one, and terminate each with a NUL marker. + side_data->clear(); + side_data->insert(side_data->end(), id_begin, id_end); + side_data->push_back(0); + side_data->insert(side_data->end(), settings_begin, settings_end); + side_data->push_back(0); +} + +} // namespace media + +#endif // MEDIA_FILTERS_WEBVTT_UTIL_H_ diff --git a/packager/media/formats/webm/cluster_builder.cc b/packager/media/formats/webm/cluster_builder.cc new file mode 100644 index 0000000000..1a3b358ef9 --- /dev/null +++ b/packager/media/formats/webm/cluster_builder.cc @@ -0,0 +1,226 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "media/formats/webm/cluster_builder.h" + +#include "base/logging.h" +#include "media/base/data_buffer.h" +#include "media/formats/webm/webm_constants.h" + +namespace media { + +static const uint8 kClusterHeader[] = { + 0x1F, 0x43, 0xB6, 0x75, // CLUSTER ID + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // cluster(size = 0) + 0xE7, // Timecode ID + 0x88, // timecode(size=8) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // timecode value +}; + +static const uint8 kSimpleBlockHeader[] = { + 0xA3, // SimpleBlock ID + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // SimpleBlock(size = 0) +}; + +static const uint8 kBlockGroupHeader[] = { + 0xA0, // BlockGroup ID + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // BlockGroup(size = 0) + 0x9B, // BlockDuration ID + 0x88, // BlockDuration(size = 8) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // duration + 0xA1, // Block ID + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Block(size = 0) +}; + +static const uint8 kBlockGroupHeaderWithoutBlockDuration[] = { + 0xA0, // BlockGroup ID + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // BlockGroup(size = 0) + 0xA1, // Block ID + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Block(size = 0) +}; + +enum { + kClusterSizeOffset = 4, + kClusterTimecodeOffset = 14, + + kSimpleBlockSizeOffset = 1, + + kBlockGroupSizeOffset = 1, + kBlockGroupWithoutBlockDurationBlockSizeOffset = 10, + kBlockGroupDurationOffset = 11, + kBlockGroupBlockSizeOffset = 20, + + kInitialBufferSize = 32768, +}; + +Cluster::Cluster(scoped_ptr data, int size) + : data_(data.Pass()), size_(size) {} +Cluster::~Cluster() {} + +ClusterBuilder::ClusterBuilder() { Reset(); } +ClusterBuilder::~ClusterBuilder() {} + +void ClusterBuilder::SetClusterTimecode(int64 cluster_timecode) { + DCHECK_EQ(cluster_timecode_, -1); + + cluster_timecode_ = cluster_timecode; + + // Write the timecode into the header. + uint8* buf = buffer_.get() + kClusterTimecodeOffset; + for (int i = 7; i >= 0; --i) { + buf[i] = cluster_timecode & 0xff; + cluster_timecode >>= 8; + } +} + +void ClusterBuilder::AddSimpleBlock(int track_num, int64 timecode, int flags, + const uint8* data, int size) { + int block_size = size + 4; + int bytes_needed = sizeof(kSimpleBlockHeader) + block_size; + if (bytes_needed > (buffer_size_ - bytes_used_)) + ExtendBuffer(bytes_needed); + + uint8* buf = buffer_.get() + bytes_used_; + int block_offset = bytes_used_; + memcpy(buf, kSimpleBlockHeader, sizeof(kSimpleBlockHeader)); + UpdateUInt64(block_offset + kSimpleBlockSizeOffset, block_size); + buf += sizeof(kSimpleBlockHeader); + + WriteBlock(buf, track_num, timecode, flags, data, size); + + bytes_used_ += bytes_needed; +} + +void ClusterBuilder::AddBlockGroup(int track_num, int64 timecode, int duration, + int flags, const uint8* data, int size) { + AddBlockGroupInternal(track_num, timecode, true, duration, flags, data, size); +} + +void ClusterBuilder::AddBlockGroupWithoutBlockDuration(int track_num, + int64 timecode, + int flags, + const uint8* data, + int size) { + AddBlockGroupInternal(track_num, timecode, false, 0, flags, data, size); +} + + +void ClusterBuilder::AddBlockGroupInternal(int track_num, int64 timecode, + bool include_block_duration, + int duration, int flags, + const uint8* data, int size) { + int block_size = size + 4; + int bytes_needed = block_size; + if (include_block_duration) { + bytes_needed += sizeof(kBlockGroupHeader); + } else { + bytes_needed += sizeof(kBlockGroupHeaderWithoutBlockDuration); + } + + int block_group_size = bytes_needed - 9; + + if (bytes_needed > (buffer_size_ - bytes_used_)) + ExtendBuffer(bytes_needed); + + uint8* buf = buffer_.get() + bytes_used_; + int block_group_offset = bytes_used_; + if (include_block_duration) { + memcpy(buf, kBlockGroupHeader, sizeof(kBlockGroupHeader)); + UpdateUInt64(block_group_offset + kBlockGroupDurationOffset, duration); + UpdateUInt64(block_group_offset + kBlockGroupBlockSizeOffset, block_size); + buf += sizeof(kBlockGroupHeader); + } else { + memcpy(buf, kBlockGroupHeaderWithoutBlockDuration, + sizeof(kBlockGroupHeaderWithoutBlockDuration)); + UpdateUInt64( + block_group_offset + kBlockGroupWithoutBlockDurationBlockSizeOffset, + block_size); + buf += sizeof(kBlockGroupHeaderWithoutBlockDuration); + } + + UpdateUInt64(block_group_offset + kBlockGroupSizeOffset, block_group_size); + + // Make sure the 4 most-significant bits are 0. + // http://www.matroska.org/technical/specs/index.html#block_structure + flags &= 0x0f; + + WriteBlock(buf, track_num, timecode, flags, data, size); + + bytes_used_ += bytes_needed; +} + +void ClusterBuilder::WriteBlock(uint8* buf, int track_num, int64 timecode, + int flags, const uint8* data, int size) { + DCHECK_GE(track_num, 0); + DCHECK_LE(track_num, 126); + DCHECK_GE(flags, 0); + DCHECK_LE(flags, 0xff); + DCHECK(data); + DCHECK_GT(size, 0); + DCHECK_NE(cluster_timecode_, -1); + + int64 timecode_delta = timecode - cluster_timecode_; + DCHECK_GE(timecode_delta, -32768); + DCHECK_LE(timecode_delta, 32767); + + buf[0] = 0x80 | (track_num & 0x7F); + buf[1] = (timecode_delta >> 8) & 0xff; + buf[2] = timecode_delta & 0xff; + buf[3] = flags & 0xff; + memcpy(buf + 4, data, size); +} + +scoped_ptr ClusterBuilder::Finish() { + DCHECK_NE(cluster_timecode_, -1); + + UpdateUInt64(kClusterSizeOffset, bytes_used_ - (kClusterSizeOffset + 8)); + + scoped_ptr ret(new Cluster(buffer_.Pass(), bytes_used_)); + Reset(); + return ret.Pass(); +} + +scoped_ptr ClusterBuilder::FinishWithUnknownSize() { + DCHECK_NE(cluster_timecode_, -1); + + UpdateUInt64(kClusterSizeOffset, kWebMUnknownSize); + + scoped_ptr ret(new Cluster(buffer_.Pass(), bytes_used_)); + Reset(); + return ret.Pass(); +} + +void ClusterBuilder::Reset() { + buffer_size_ = kInitialBufferSize; + buffer_.reset(new uint8[buffer_size_]); + memcpy(buffer_.get(), kClusterHeader, sizeof(kClusterHeader)); + bytes_used_ = sizeof(kClusterHeader); + cluster_timecode_ = -1; +} + +void ClusterBuilder::ExtendBuffer(int bytes_needed) { + int new_buffer_size = 2 * buffer_size_; + + while ((new_buffer_size - bytes_used_) < bytes_needed) + new_buffer_size *= 2; + + scoped_ptr new_buffer(new uint8[new_buffer_size]); + + memcpy(new_buffer.get(), buffer_.get(), bytes_used_); + buffer_.reset(new_buffer.release()); + buffer_size_ = new_buffer_size; +} + +void ClusterBuilder::UpdateUInt64(int offset, int64 value) { + DCHECK_LE(offset + 7, buffer_size_); + uint8* buf = buffer_.get() + offset; + + // Fill the last 7 bytes of size field in big-endian order. + for (int i = 7; i > 0; i--) { + buf[i] = value & 0xff; + value >>= 8; + } +} + +} // namespace media diff --git a/packager/media/formats/webm/cluster_builder.h b/packager/media/formats/webm/cluster_builder.h new file mode 100644 index 0000000000..f6f6001849 --- /dev/null +++ b/packager/media/formats/webm/cluster_builder.h @@ -0,0 +1,64 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef MEDIA_FORMATS_WEBM_CLUSTER_BUILDER_H_ +#define MEDIA_FORMATS_WEBM_CLUSTER_BUILDER_H_ + +#include "base/basictypes.h" +#include "base/memory/scoped_ptr.h" + +namespace media { + +class Cluster { + public: + Cluster(scoped_ptr data, int size); + ~Cluster(); + + const uint8* data() const { return data_.get(); } + int size() const { return size_; } + + private: + scoped_ptr data_; + int size_; + + DISALLOW_IMPLICIT_CONSTRUCTORS(Cluster); +}; + +class ClusterBuilder { + public: + ClusterBuilder(); + ~ClusterBuilder(); + + void SetClusterTimecode(int64 cluster_timecode); + void AddSimpleBlock(int track_num, int64 timecode, int flags, + const uint8* data, int size); + void AddBlockGroup(int track_num, int64 timecode, int duration, int flags, + const uint8* data, int size); + void AddBlockGroupWithoutBlockDuration(int track_num, int64 timecode, + int flags, const uint8* data, int size); + + scoped_ptr Finish(); + scoped_ptr FinishWithUnknownSize(); + + private: + void AddBlockGroupInternal(int track_num, int64 timecode, + bool include_block_duration, int duration, + int flags, const uint8* data, int size); + void Reset(); + void ExtendBuffer(int bytes_needed); + void UpdateUInt64(int offset, int64 value); + void WriteBlock(uint8* buf, int track_num, int64 timecode, int flags, + const uint8* data, int size); + + scoped_ptr buffer_; + int buffer_size_; + int bytes_used_; + int64 cluster_timecode_; + + DISALLOW_COPY_AND_ASSIGN(ClusterBuilder); +}; + +} // namespace media + +#endif // MEDIA_FORMATS_WEBM_CLUSTER_BUILDER_H_ diff --git a/packager/media/formats/webm/opus_packet_builder.cc b/packager/media/formats/webm/opus_packet_builder.cc new file mode 100644 index 0000000000..b2dd8dca1e --- /dev/null +++ b/packager/media/formats/webm/opus_packet_builder.cc @@ -0,0 +1,89 @@ +// Copyright (c) 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/logging.h" +#include "media/formats/webm/opus_packet_builder.h" +#include "media/formats/webm/webm_cluster_parser.h" + +namespace media { + +OpusPacket::OpusPacket(uint8_t config, uint8_t frame_count, bool is_VBR) { + DCHECK_GE(config, 0); + DCHECK_LT(config, kNumPossibleOpusConfigs); + DCHECK_GE(frame_count, kMinOpusPacketFrameCount); + DCHECK_LE(frame_count, kMaxOpusPacketFrameCount); + + duration_ms_ = frame_count * + WebMClusterParser::kOpusFrameDurationsMu[config] / + static_cast(1000); + + uint8_t frame_count_code; + uint8_t frame_count_byte; + + if (frame_count == 1) { + frame_count_code = 0; + } else if (frame_count == 2) { + frame_count_code = is_VBR ? 2 : 1; + } else { + frame_count_code = 3; + frame_count_byte = (is_VBR ? 1 << 7 : 0) | frame_count; + } + + // All opus packets must have TOC byte. + uint8_t opus_toc_byte = (config << 3) | frame_count_code; + data_.push_back(opus_toc_byte); + + // For code 3 packets, the number of frames is signaled in the "frame + // count byte". + if (frame_count_code == 3) { + data_.push_back(frame_count_byte); + } + + // Packet will only conform to layout specification for the TOC byte + // and optional frame count bytes appended above. This last byte + // is purely dummy padding where frame size data or encoded data might + // otherwise start. + data_.push_back(static_cast(0)); +} + +OpusPacket::~OpusPacket() { +} + +const uint8_t* OpusPacket::data() const { + return &(data_[0]); +} + +int OpusPacket::size() const { + return data_.size(); +} + +double OpusPacket::duration_ms() const { + return duration_ms_; +} + +ScopedVector BuildAllOpusPackets() { + ScopedVector opus_packets; + + for (int frame_count = kMinOpusPacketFrameCount; + frame_count <= kMaxOpusPacketFrameCount; frame_count++) { + for (int opus_config_num = 0; opus_config_num < kNumPossibleOpusConfigs; + opus_config_num++) { + bool is_VBR = false; + opus_packets.push_back( + new OpusPacket(opus_config_num, frame_count, is_VBR)); + + if (frame_count >= 2) { + // Add another packet with VBR flag toggled. For frame counts >= 2, + // VBR triggers changes to packet framing. + is_VBR = true; + opus_packets.push_back( + new OpusPacket(opus_config_num, frame_count, is_VBR)); + } + } + } + + return opus_packets.Pass(); +} + +} // namespace media diff --git a/packager/media/formats/webm/opus_packet_builder.h b/packager/media/formats/webm/opus_packet_builder.h new file mode 100644 index 0000000000..af76b49a39 --- /dev/null +++ b/packager/media/formats/webm/opus_packet_builder.h @@ -0,0 +1,43 @@ +// Copyright (c) 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef MEDIA_FORMATS_WEBM_OPUS_PACKET_BUILDER_H_ +#define MEDIA_FORMATS_WEBM_OPUS_PACKET_BUILDER_H_ + +#include + +#include "base/memory/scoped_ptr.h" +#include "base/memory/scoped_vector.h" + +namespace media { + +// From Opus RFC. See https://tools.ietf.org/html/rfc6716#page-14 +enum OpusConstants { + kNumPossibleOpusConfigs = 32, + kMinOpusPacketFrameCount = 1, + kMaxOpusPacketFrameCount = 48 +}; + +class OpusPacket { + public: + OpusPacket(uint8_t config, uint8_t frame_count, bool is_VBR); + ~OpusPacket(); + + const uint8_t* data() const; + int size() const; + double duration_ms() const; + + private: + std::vector data_; + double duration_ms_; + + DISALLOW_COPY_AND_ASSIGN(OpusPacket); +}; + +// Builds an exhaustive collection of Opus packet configurations. +ScopedVector BuildAllOpusPackets(); + +} // namespace media + +#endif // MEDIA_FORMATS_WEBM_OPUS_PACKET_BUILDER_H_ diff --git a/packager/media/formats/webm/tracks_builder.cc b/packager/media/formats/webm/tracks_builder.cc new file mode 100644 index 0000000000..927c2a4f2b --- /dev/null +++ b/packager/media/formats/webm/tracks_builder.cc @@ -0,0 +1,386 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "media/formats/webm/tracks_builder.h" + +#include "base/logging.h" +#include "media/formats/webm/webm_constants.h" + +namespace media { + +// Returns size of an integer, formatted using Matroska serialization. +static int GetUIntMkvSize(uint64 value) { + if (value < 0x07FULL) + return 1; + if (value < 0x03FFFULL) + return 2; + if (value < 0x01FFFFFULL) + return 3; + if (value < 0x0FFFFFFFULL) + return 4; + if (value < 0x07FFFFFFFFULL) + return 5; + if (value < 0x03FFFFFFFFFFULL) + return 6; + if (value < 0x01FFFFFFFFFFFFULL) + return 7; + return 8; +} + +// Returns the minimium size required to serialize an integer value. +static int GetUIntSize(uint64 value) { + if (value < 0x0100ULL) + return 1; + if (value < 0x010000ULL) + return 2; + if (value < 0x01000000ULL) + return 3; + if (value < 0x0100000000ULL) + return 4; + if (value < 0x010000000000ULL) + return 5; + if (value < 0x01000000000000ULL) + return 6; + if (value < 0x0100000000000000ULL) + return 7; + return 8; +} + +static int MasterElementSize(int element_id, int payload_size) { + return GetUIntSize(element_id) + GetUIntMkvSize(payload_size) + payload_size; +} + +static int UIntElementSize(int element_id, uint64 value) { + return GetUIntSize(element_id) + 1 + GetUIntSize(value); +} + +static int DoubleElementSize(int element_id) { + return GetUIntSize(element_id) + 1 + 8; +} + +static int StringElementSize(int element_id, const std::string& value) { + return GetUIntSize(element_id) + + GetUIntMkvSize(value.length()) + + value.length(); +} + +static void SerializeInt(uint8** buf_ptr, int* buf_size_ptr, + int64 value, int size) { + uint8*& buf = *buf_ptr; + int& buf_size = *buf_size_ptr; + + for (int idx = 1; idx <= size; ++idx) { + *buf++ = static_cast(value >> ((size - idx) * 8)); + --buf_size; + } +} + +static void SerializeDouble(uint8** buf_ptr, int* buf_size_ptr, + double value) { + // Use a union to convert |value| to native endian integer bit pattern. + union { + double src; + int64 dst; + } tmp; + tmp.src = value; + + // Write the bytes from native endian |tmp.dst| to big-endian form in |buf|. + SerializeInt(buf_ptr, buf_size_ptr, tmp.dst, 8); +} + +static void WriteElementId(uint8** buf, int* buf_size, int element_id) { + SerializeInt(buf, buf_size, element_id, GetUIntSize(element_id)); +} + +static void WriteUInt(uint8** buf, int* buf_size, uint64 value) { + const int size = GetUIntMkvSize(value); + value |= (1ULL << (size * 7)); // Matroska formatting + SerializeInt(buf, buf_size, value, size); +} + +static void WriteMasterElement(uint8** buf, int* buf_size, + int element_id, int payload_size) { + WriteElementId(buf, buf_size, element_id); + WriteUInt(buf, buf_size, payload_size); +} + +static void WriteUIntElement(uint8** buf, + int* buf_size, + int element_id, + uint64 value) { + WriteElementId(buf, buf_size, element_id); + + const int size = GetUIntSize(value); + WriteUInt(buf, buf_size, size); + + SerializeInt(buf, buf_size, value, size); +} + +static void WriteDoubleElement(uint8** buf, int* buf_size, + int element_id, double value) { + WriteElementId(buf, buf_size, element_id); + WriteUInt(buf, buf_size, 8); + SerializeDouble(buf, buf_size, value); +} + +static void WriteStringElement(uint8** buf_ptr, int* buf_size_ptr, + int element_id, const std::string& value) { + uint8*& buf = *buf_ptr; + int& buf_size = *buf_size_ptr; + + WriteElementId(&buf, &buf_size, element_id); + + const uint64 size = value.length(); + WriteUInt(&buf, &buf_size, size); + + memcpy(buf, value.data(), size); + buf += size; + buf_size -= size; +} + +TracksBuilder::TracksBuilder(bool allow_invalid_values) + : allow_invalid_values_(allow_invalid_values) {} +TracksBuilder::TracksBuilder() + : allow_invalid_values_(false) {} +TracksBuilder::~TracksBuilder() {} + +void TracksBuilder::AddVideoTrack(int track_num, + uint64 track_uid, + const std::string& codec_id, + const std::string& name, + const std::string& language, + int default_duration, + int video_pixel_width, + int video_pixel_height) { + AddTrackInternal(track_num, kWebMTrackTypeVideo, track_uid, codec_id, name, + language, default_duration, video_pixel_width, + video_pixel_height, -1, -1); +} + +void TracksBuilder::AddAudioTrack(int track_num, + uint64 track_uid, + const std::string& codec_id, + const std::string& name, + const std::string& language, + int default_duration, + int audio_channels, + double audio_sampling_frequency) { + AddTrackInternal(track_num, kWebMTrackTypeAudio, track_uid, codec_id, name, + language, default_duration, -1, -1, audio_channels, + audio_sampling_frequency); +} + +void TracksBuilder::AddTextTrack(int track_num, + uint64 track_uid, + const std::string& codec_id, + const std::string& name, + const std::string& language) { + AddTrackInternal(track_num, kWebMTrackTypeSubtitlesOrCaptions, track_uid, + codec_id, name, language, -1, -1, -1, -1, -1); +} + +std::vector TracksBuilder::Finish() { + // Allocate the storage + std::vector buffer; + buffer.resize(GetTracksSize()); + + // Populate the storage with a tracks header + WriteTracks(&buffer[0], buffer.size()); + + return buffer; +} + +void TracksBuilder::AddTrackInternal(int track_num, + int track_type, + uint64 track_uid, + const std::string& codec_id, + const std::string& name, + const std::string& language, + int default_duration, + int video_pixel_width, + int video_pixel_height, + int audio_channels, + double audio_sampling_frequency) { + tracks_.push_back(Track(track_num, track_type, track_uid, codec_id, name, + language, default_duration, video_pixel_width, + video_pixel_height, audio_channels, + audio_sampling_frequency, allow_invalid_values_)); +} + +int TracksBuilder::GetTracksSize() const { + return MasterElementSize(kWebMIdTracks, GetTracksPayloadSize()); +} + +int TracksBuilder::GetTracksPayloadSize() const { + int payload_size = 0; + + for (TrackList::const_iterator itr = tracks_.begin(); + itr != tracks_.end(); ++itr) { + payload_size += itr->GetSize(); + } + + return payload_size; +} + +void TracksBuilder::WriteTracks(uint8* buf, int buf_size) const { + WriteMasterElement(&buf, &buf_size, kWebMIdTracks, GetTracksPayloadSize()); + + for (TrackList::const_iterator itr = tracks_.begin(); + itr != tracks_.end(); ++itr) { + itr->Write(&buf, &buf_size); + } +} + +TracksBuilder::Track::Track(int track_num, + int track_type, + uint64 track_uid, + const std::string& codec_id, + const std::string& name, + const std::string& language, + int default_duration, + int video_pixel_width, + int video_pixel_height, + int audio_channels, + double audio_sampling_frequency, + bool allow_invalid_values) + : track_num_(track_num), + track_type_(track_type), + track_uid_(track_uid), + codec_id_(codec_id), + name_(name), + language_(language), + default_duration_(default_duration), + video_pixel_width_(video_pixel_width), + video_pixel_height_(video_pixel_height), + audio_channels_(audio_channels), + audio_sampling_frequency_(audio_sampling_frequency) { + if (!allow_invalid_values) { + CHECK_GT(track_num_, 0); + CHECK_GT(track_type_, 0); + CHECK_LT(track_type_, 255); + CHECK_GT(track_uid_, 0); + if (track_type != kWebMTrackTypeVideo && + track_type != kWebMTrackTypeAudio) { + CHECK_EQ(default_duration_, -1); + } else { + CHECK(default_duration_ == -1 || default_duration_ > 0); + } + + if (track_type == kWebMTrackTypeVideo) { + CHECK_GT(video_pixel_width_, 0); + CHECK_GT(video_pixel_height_, 0); + } else { + CHECK_EQ(video_pixel_width_, -1); + CHECK_EQ(video_pixel_height_, -1); + } + + if (track_type == kWebMTrackTypeAudio) { + CHECK_GT(audio_channels_, 0); + CHECK_GT(audio_sampling_frequency_, 0.0); + } else { + CHECK_EQ(audio_channels_, -1); + CHECK_EQ(audio_sampling_frequency_, -1.0); + } + } +} + +int TracksBuilder::Track::GetSize() const { + return MasterElementSize(kWebMIdTrackEntry, GetPayloadSize()); +} + +int TracksBuilder::Track::GetVideoPayloadSize() const { + int payload_size = 0; + + if (video_pixel_width_ >= 0) + payload_size += UIntElementSize(kWebMIdPixelWidth, video_pixel_width_); + if (video_pixel_height_ >= 0) + payload_size += UIntElementSize(kWebMIdPixelHeight, video_pixel_height_); + + return payload_size; +} + +int TracksBuilder::Track::GetAudioPayloadSize() const { + int payload_size = 0; + + if (audio_channels_ >= 0) + payload_size += UIntElementSize(kWebMIdChannels, audio_channels_); + if (audio_sampling_frequency_ >= 0) + payload_size += DoubleElementSize(kWebMIdSamplingFrequency); + + return payload_size; +} + +int TracksBuilder::Track::GetPayloadSize() const { + int size = 0; + + size += UIntElementSize(kWebMIdTrackNumber, track_num_); + size += UIntElementSize(kWebMIdTrackType, track_type_); + size += UIntElementSize(kWebMIdTrackUID, track_uid_); + + if (default_duration_ >= 0) + size += UIntElementSize(kWebMIdDefaultDuration, default_duration_); + + if (!codec_id_.empty()) + size += StringElementSize(kWebMIdCodecID, codec_id_); + + if (!name_.empty()) + size += StringElementSize(kWebMIdName, name_); + + if (!language_.empty()) + size += StringElementSize(kWebMIdLanguage, language_); + + if (GetVideoPayloadSize() > 0) { + size += MasterElementSize(kWebMIdVideo, GetVideoPayloadSize()); + } + + if (GetAudioPayloadSize() > 0) { + size += MasterElementSize(kWebMIdAudio, GetAudioPayloadSize()); + } + + return size; +} + +void TracksBuilder::Track::Write(uint8** buf, int* buf_size) const { + WriteMasterElement(buf, buf_size, kWebMIdTrackEntry, GetPayloadSize()); + + WriteUIntElement(buf, buf_size, kWebMIdTrackNumber, track_num_); + WriteUIntElement(buf, buf_size, kWebMIdTrackType, track_type_); + WriteUIntElement(buf, buf_size, kWebMIdTrackUID, track_uid_); + + if (default_duration_ >= 0) + WriteUIntElement(buf, buf_size, kWebMIdDefaultDuration, default_duration_); + + if (!codec_id_.empty()) + WriteStringElement(buf, buf_size, kWebMIdCodecID, codec_id_); + + if (!name_.empty()) + WriteStringElement(buf, buf_size, kWebMIdName, name_); + + if (!language_.empty()) + WriteStringElement(buf, buf_size, kWebMIdLanguage, language_); + + if (GetVideoPayloadSize() > 0) { + WriteMasterElement(buf, buf_size, kWebMIdVideo, GetVideoPayloadSize()); + + if (video_pixel_width_ >= 0) + WriteUIntElement(buf, buf_size, kWebMIdPixelWidth, video_pixel_width_); + + if (video_pixel_height_ >= 0) + WriteUIntElement(buf, buf_size, kWebMIdPixelHeight, video_pixel_height_); + } + + if (GetAudioPayloadSize() > 0) { + WriteMasterElement(buf, buf_size, kWebMIdAudio, GetAudioPayloadSize()); + + if (audio_channels_ >= 0) + WriteUIntElement(buf, buf_size, kWebMIdChannels, audio_channels_); + + if (audio_sampling_frequency_ >= 0) { + WriteDoubleElement(buf, buf_size, kWebMIdSamplingFrequency, + audio_sampling_frequency_); + } + } +} + +} // namespace media diff --git a/packager/media/formats/webm/tracks_builder.h b/packager/media/formats/webm/tracks_builder.h new file mode 100644 index 0000000000..f7786fd70f --- /dev/null +++ b/packager/media/formats/webm/tracks_builder.h @@ -0,0 +1,114 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef MEDIA_FORMATS_WEBM_TRACKS_BUILDER_H_ +#define MEDIA_FORMATS_WEBM_TRACKS_BUILDER_H_ + +#include +#include +#include + +#include "base/basictypes.h" + +namespace media { + +class TracksBuilder { + public: + // If |allow_invalid_values| is false, some AddTrack() parameters will be + // basically checked and will assert if out of valid range. |codec_id|, + // |name|, |language| and any device-specific constraints are not checked. + explicit TracksBuilder(bool allow_invalid_values); + TracksBuilder(); // Sets |allow_invalid_values| to false. + ~TracksBuilder(); + + // Only a non-negative |default_duration| will result in a serialized + // kWebMIdDefaultDuration element. Note, 0 is allowed here for testing only + // if |allow_invalid_values_| is true, since it is an illegal value for + // DefaultDuration. Similar applies to |audio_channels|, + // |audio_sampling_frequency|, |video_pixel_width| and |video_pixel_height|. + void AddVideoTrack(int track_num, + uint64 track_uid, + const std::string& codec_id, + const std::string& name, + const std::string& language, + int default_duration, + int video_pixel_width, + int video_pixel_height); + void AddAudioTrack(int track_num, + uint64 track_uid, + const std::string& codec_id, + const std::string& name, + const std::string& language, + int default_duration, + int audio_channels, + double audio_sampling_frequency); + void AddTextTrack(int track_num, + uint64 track_uid, + const std::string& codec_id, + const std::string& name, + const std::string& language); + + std::vector Finish(); + + private: + void AddTrackInternal(int track_num, + int track_type, + uint64 track_uid, + const std::string& codec_id, + const std::string& name, + const std::string& language, + int default_duration, + int video_pixel_width, + int video_pixel_height, + int audio_channels, + double audio_sampling_frequency); + int GetTracksSize() const; + int GetTracksPayloadSize() const; + void WriteTracks(uint8* buffer, int buffer_size) const; + + class Track { + public: + Track(int track_num, + int track_type, + uint64 track_uid, + const std::string& codec_id, + const std::string& name, + const std::string& language, + int default_duration, + int video_pixel_width, + int video_pixel_height, + int audio_channels, + double audio_sampling_frequency, + bool allow_invalid_values); + + int GetSize() const; + void Write(uint8** buf, int* buf_size) const; + private: + int GetPayloadSize() const; + int GetVideoPayloadSize() const; + int GetAudioPayloadSize() const; + + int track_num_; + int track_type_; + int track_uid_; + std::string codec_id_; + std::string name_; + std::string language_; + int default_duration_; + int video_pixel_width_; + int video_pixel_height_; + int audio_channels_; + double audio_sampling_frequency_; + }; + + typedef std::list TrackList; + TrackList tracks_; + bool allow_invalid_values_; + + DISALLOW_COPY_AND_ASSIGN(TracksBuilder); +}; + +} // namespace media + +#endif // MEDIA_FORMATS_WEBM_TRACKS_BUILDER_H_ diff --git a/packager/media/formats/webm/webm_audio_client.cc b/packager/media/formats/webm/webm_audio_client.cc new file mode 100644 index 0000000000..bde7233254 --- /dev/null +++ b/packager/media/formats/webm/webm_audio_client.cc @@ -0,0 +1,141 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "media/formats/webm/webm_audio_client.h" + +#include "media/base/audio_decoder_config.h" +#include "media/base/channel_layout.h" +#include "media/formats/webm/webm_constants.h" + +namespace media { + +WebMAudioClient::WebMAudioClient(const scoped_refptr& media_log) + : media_log_(media_log) { + Reset(); +} + +WebMAudioClient::~WebMAudioClient() { +} + +void WebMAudioClient::Reset() { + channels_ = -1; + samples_per_second_ = -1; + output_samples_per_second_ = -1; +} + +bool WebMAudioClient::InitializeConfig( + const std::string& codec_id, const std::vector& codec_private, + int64 seek_preroll, int64 codec_delay, bool is_encrypted, + AudioDecoderConfig* config) { + DCHECK(config); + SampleFormat sample_format = kSampleFormatPlanarF32; + + AudioCodec audio_codec = kUnknownAudioCodec; + if (codec_id == "A_VORBIS") { + audio_codec = kCodecVorbis; + } else if (codec_id == "A_OPUS") { + audio_codec = kCodecOpus; + } else { + MEDIA_LOG(ERROR, media_log_) << "Unsupported audio codec_id " << codec_id; + return false; + } + + if (samples_per_second_ <= 0) + return false; + + // Set channel layout default if a Channels element was not present. + if (channels_ == -1) + channels_ = 1; + + ChannelLayout channel_layout = GuessChannelLayout(channels_); + + if (channel_layout == CHANNEL_LAYOUT_UNSUPPORTED) { + MEDIA_LOG(ERROR, media_log_) << "Unsupported channel count " << channels_; + return false; + } + + int samples_per_second = samples_per_second_; + if (output_samples_per_second_ > 0) + samples_per_second = output_samples_per_second_; + + // Always use 48kHz for OPUS. See the "Input Sample Rate" section of the + // spec: http://tools.ietf.org/html/draft-terriberry-oggopus-01#page-11 + if (audio_codec == kCodecOpus) { + samples_per_second = 48000; + sample_format = kSampleFormatF32; + } + + const uint8* extra_data = NULL; + size_t extra_data_size = 0; + if (codec_private.size() > 0) { + extra_data = &codec_private[0]; + extra_data_size = codec_private.size(); + } + + // Convert |codec_delay| from nanoseconds into frames. + int codec_delay_in_frames = 0; + if (codec_delay != -1) { + codec_delay_in_frames = + 0.5 + + samples_per_second * (static_cast(codec_delay) / + base::Time::kNanosecondsPerSecond); + } + + config->Initialize( + audio_codec, + sample_format, + channel_layout, + samples_per_second, + extra_data, + extra_data_size, + is_encrypted, + base::TimeDelta::FromMicroseconds( + (seek_preroll != -1 ? seek_preroll : 0) / 1000), + codec_delay_in_frames); + return config->IsValidConfig(); +} + +bool WebMAudioClient::OnUInt(int id, int64 val) { + if (id == kWebMIdChannels) { + if (channels_ != -1) { + MEDIA_LOG(ERROR, media_log_) << "Multiple values for id " << std::hex + << id << " specified. (" << channels_ + << " and " << val << ")"; + return false; + } + + channels_ = val; + } + return true; +} + +bool WebMAudioClient::OnFloat(int id, double val) { + double* dst = NULL; + + switch (id) { + case kWebMIdSamplingFrequency: + dst = &samples_per_second_; + break; + case kWebMIdOutputSamplingFrequency: + dst = &output_samples_per_second_; + break; + default: + return true; + } + + if (val <= 0) + return false; + + if (*dst != -1) { + MEDIA_LOG(ERROR, media_log_) << "Multiple values for id " << std::hex << id + << " specified (" << *dst << " and " << val + << ")"; + return false; + } + + *dst = val; + return true; +} + +} // namespace media diff --git a/packager/media/formats/webm/webm_audio_client.h b/packager/media/formats/webm/webm_audio_client.h new file mode 100644 index 0000000000..67f5f01aae --- /dev/null +++ b/packager/media/formats/webm/webm_audio_client.h @@ -0,0 +1,54 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef MEDIA_FORMATS_WEBM_WEBM_AUDIO_CLIENT_H_ +#define MEDIA_FORMATS_WEBM_WEBM_AUDIO_CLIENT_H_ + +#include +#include + +#include "media/base/media_log.h" +#include "media/formats/webm/webm_parser.h" + +namespace media { +class AudioDecoderConfig; + +// Helper class used to parse an Audio element inside a TrackEntry element. +class WebMAudioClient : public WebMParserClient { + public: + explicit WebMAudioClient(const scoped_refptr& media_log); + ~WebMAudioClient() override; + + // Reset this object's state so it can process a new audio track element. + void Reset(); + + // Initialize |config| with the data in |codec_id|, |codec_private|, + // |is_encrypted| and the fields parsed from the last audio track element this + // object was used to parse. + // Returns true if |config| was successfully initialized. + // Returns false if there was unexpected values in the provided parameters or + // audio track element fields. + bool InitializeConfig(const std::string& codec_id, + const std::vector& codec_private, + const int64 seek_preroll, + const int64 codec_delay, + bool is_encrypted, + AudioDecoderConfig* config); + + private: + // WebMParserClient implementation. + bool OnUInt(int id, int64 val) override; + bool OnFloat(int id, double val) override; + + scoped_refptr media_log_; + int channels_; + double samples_per_second_; + double output_samples_per_second_; + + DISALLOW_COPY_AND_ASSIGN(WebMAudioClient); +}; + +} // namespace media + +#endif // MEDIA_FORMATS_WEBM_WEBM_AUDIO_CLIENT_H_ diff --git a/packager/media/formats/webm/webm_cluster_parser.cc b/packager/media/formats/webm/webm_cluster_parser.cc new file mode 100644 index 0000000000..ee491371d3 --- /dev/null +++ b/packager/media/formats/webm/webm_cluster_parser.cc @@ -0,0 +1,882 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "media/formats/webm/webm_cluster_parser.h" + +#include + +#include "base/logging.h" +#include "base/sys_byteorder.h" +#include "media/base/decrypt_config.h" +#include "media/base/timestamp_constants.h" +#include "media/filters/webvtt_util.h" +#include "media/formats/webm/webm_constants.h" +#include "media/formats/webm/webm_crypto_helpers.h" +#include "media/formats/webm/webm_webvtt_parser.h" + +namespace media { + +const uint16_t WebMClusterParser::kOpusFrameDurationsMu[] = { + 10000, 20000, 40000, 60000, 10000, 20000, 40000, 60000, 10000, 20000, 40000, + 60000, 10000, 20000, 10000, 20000, 2500, 5000, 10000, 20000, 2500, 5000, + 10000, 20000, 2500, 5000, 10000, 20000, 2500, 5000, 10000, 20000}; + +enum { + // Limits the number of MEDIA_LOG() calls in the path of reading encoded + // duration to avoid spamming for corrupted data. + kMaxDurationErrorLogs = 10, + // Limits the number of MEDIA_LOG() calls warning the user that buffer + // durations have been estimated. + kMaxDurationEstimateLogs = 10, +}; + +WebMClusterParser::WebMClusterParser( + int64 timecode_scale, + int audio_track_num, + base::TimeDelta audio_default_duration, + int video_track_num, + base::TimeDelta video_default_duration, + const WebMTracksParser::TextTracks& text_tracks, + const std::set& ignored_tracks, + const std::string& audio_encryption_key_id, + const std::string& video_encryption_key_id, + const AudioCodec audio_codec, + const scoped_refptr& media_log) + : timecode_multiplier_(timecode_scale / 1000.0), + ignored_tracks_(ignored_tracks), + audio_encryption_key_id_(audio_encryption_key_id), + video_encryption_key_id_(video_encryption_key_id), + audio_codec_(audio_codec), + parser_(kWebMIdCluster, this), + cluster_start_time_(kNoTimestamp()), + audio_(audio_track_num, false, audio_default_duration, media_log), + video_(video_track_num, true, video_default_duration, media_log), + ready_buffer_upper_bound_(kNoDecodeTimestamp()), + media_log_(media_log) { + for (WebMTracksParser::TextTracks::const_iterator it = text_tracks.begin(); + it != text_tracks.end(); + ++it) { + text_track_map_.insert(std::make_pair( + it->first, Track(it->first, false, kNoTimestamp(), media_log_))); + } +} + +WebMClusterParser::~WebMClusterParser() {} + +void WebMClusterParser::Reset() { + last_block_timecode_ = -1; + cluster_timecode_ = -1; + cluster_start_time_ = kNoTimestamp(); + cluster_ended_ = false; + parser_.Reset(); + audio_.Reset(); + video_.Reset(); + ResetTextTracks(); + ready_buffer_upper_bound_ = kNoDecodeTimestamp(); +} + +int WebMClusterParser::Parse(const uint8_t* buf, int size) { + audio_.ClearReadyBuffers(); + video_.ClearReadyBuffers(); + ClearTextTrackReadyBuffers(); + ready_buffer_upper_bound_ = kNoDecodeTimestamp(); + + int result = parser_.Parse(buf, size); + + if (result < 0) { + cluster_ended_ = false; + return result; + } + + cluster_ended_ = parser_.IsParsingComplete(); + if (cluster_ended_) { + // If there were no buffers in this cluster, set the cluster start time to + // be the |cluster_timecode_|. + if (cluster_start_time_ == kNoTimestamp()) { + // If the cluster did not even have a |cluster_timecode_|, signal parse + // error. + if (cluster_timecode_ < 0) + return -1; + + cluster_start_time_ = base::TimeDelta::FromMicroseconds( + cluster_timecode_ * timecode_multiplier_); + } + + // Reset the parser if we're done parsing so that + // it is ready to accept another cluster on the next + // call. + parser_.Reset(); + + last_block_timecode_ = -1; + cluster_timecode_ = -1; + } + + return result; +} + +const WebMClusterParser::BufferQueue& WebMClusterParser::GetAudioBuffers() { + if (ready_buffer_upper_bound_ == kNoDecodeTimestamp()) + UpdateReadyBuffers(); + + return audio_.ready_buffers(); +} + +const WebMClusterParser::BufferQueue& WebMClusterParser::GetVideoBuffers() { + if (ready_buffer_upper_bound_ == kNoDecodeTimestamp()) + UpdateReadyBuffers(); + + return video_.ready_buffers(); +} + +const WebMClusterParser::TextBufferQueueMap& +WebMClusterParser::GetTextBuffers() { + if (ready_buffer_upper_bound_ == kNoDecodeTimestamp()) + UpdateReadyBuffers(); + + // Translate our |text_track_map_| into |text_buffers_map_|, inserting rows in + // the output only for non-empty ready_buffer() queues in |text_track_map_|. + text_buffers_map_.clear(); + for (TextTrackMap::const_iterator itr = text_track_map_.begin(); + itr != text_track_map_.end(); + ++itr) { + const BufferQueue& text_buffers = itr->second.ready_buffers(); + if (!text_buffers.empty()) + text_buffers_map_.insert(std::make_pair(itr->first, text_buffers)); + } + + return text_buffers_map_; +} + +base::TimeDelta WebMClusterParser::TryGetEncodedAudioDuration( + const uint8_t* data, + int size) { + + // Duration is currently read assuming the *entire* stream is unencrypted. + // The special "Signal Byte" prepended to Blocks in encrypted streams is + // assumed to not be present. + // TODO(chcunningham): Consider parsing "Signal Byte" for encrypted streams + // to return duration for any unencrypted blocks. + + if (audio_codec_ == kCodecOpus) { + return ReadOpusDuration(data, size); + } + + // TODO(wolenetz/chcunningham): Implement duration reading for Vorbis. See + // motivations in http://crbug.com/396634. + + return kNoTimestamp(); +} + +base::TimeDelta WebMClusterParser::ReadOpusDuration(const uint8_t* data, + int size) { + // Masks and constants for Opus packets. See + // https://tools.ietf.org/html/rfc6716#page-14 + static const uint8_t kTocConfigMask = 0xf8; + static const uint8_t kTocFrameCountCodeMask = 0x03; + static const uint8_t kFrameCountMask = 0x3f; + static const base::TimeDelta kPacketDurationMax = + base::TimeDelta::FromMilliseconds(120); + + if (size < 1) { + LIMITED_MEDIA_LOG(DEBUG, media_log_, num_duration_errors_, + kMaxDurationErrorLogs) + << "Invalid zero-byte Opus packet; demuxed block duration may be " + "imprecise."; + return kNoTimestamp(); + } + + // Frame count type described by last 2 bits of Opus TOC byte. + int frame_count_type = data[0] & kTocFrameCountCodeMask; + + int frame_count = 0; + switch (frame_count_type) { + case 0: + frame_count = 1; + break; + case 1: + case 2: + frame_count = 2; + break; + case 3: + // Type 3 indicates an arbitrary frame count described in the next byte. + if (size < 2) { + LIMITED_MEDIA_LOG(DEBUG, media_log_, num_duration_errors_, + kMaxDurationErrorLogs) + << "Second byte missing from 'Code 3' Opus packet; demuxed block " + "duration may be imprecise."; + return kNoTimestamp(); + } + + frame_count = data[1] & kFrameCountMask; + + if (frame_count == 0) { + LIMITED_MEDIA_LOG(DEBUG, media_log_, num_duration_errors_, + kMaxDurationErrorLogs) + << "Illegal 'Code 3' Opus packet with frame count zero; demuxed " + "block duration may be imprecise."; + return kNoTimestamp(); + } + + break; + default: + LIMITED_MEDIA_LOG(DEBUG, media_log_, num_duration_errors_, + kMaxDurationErrorLogs) + << "Unexpected Opus frame count type: " << frame_count_type << "; " + << "demuxed block duration may be imprecise."; + return kNoTimestamp(); + } + + int opusConfig = (data[0] & kTocConfigMask) >> 3; + CHECK_GE(opusConfig, 0); + CHECK_LT(opusConfig, static_cast(arraysize(kOpusFrameDurationsMu))); + + DCHECK_GT(frame_count, 0); + base::TimeDelta duration = base::TimeDelta::FromMicroseconds( + kOpusFrameDurationsMu[opusConfig] * frame_count); + + if (duration > kPacketDurationMax) { + // Intentionally allowing packet to pass through for now. Decoder should + // either handle or fail gracefully. MEDIA_LOG as breadcrumbs in case + // things go sideways. + LIMITED_MEDIA_LOG(DEBUG, media_log_, num_duration_errors_, + kMaxDurationErrorLogs) + << "Warning, demuxed Opus packet with encoded duration: " + << duration.InMilliseconds() << "ms. Should be no greater than " + << kPacketDurationMax.InMilliseconds() << "ms."; + } + + return duration; +} + +WebMParserClient* WebMClusterParser::OnListStart(int id) { + if (id == kWebMIdCluster) { + cluster_timecode_ = -1; + cluster_start_time_ = kNoTimestamp(); + } else if (id == kWebMIdBlockGroup) { + block_data_.reset(); + block_data_size_ = -1; + block_duration_ = -1; + discard_padding_ = -1; + discard_padding_set_ = false; + } else if (id == kWebMIdBlockAdditions) { + block_add_id_ = -1; + block_additional_data_.reset(); + block_additional_data_size_ = 0; + } + + return this; +} + +bool WebMClusterParser::OnListEnd(int id) { + if (id != kWebMIdBlockGroup) + return true; + + // Make sure the BlockGroup actually had a Block. + if (block_data_size_ == -1) { + MEDIA_LOG(ERROR, media_log_) << "Block missing from BlockGroup."; + return false; + } + + bool result = ParseBlock(false, block_data_.get(), block_data_size_, + block_additional_data_.get(), + block_additional_data_size_, block_duration_, + discard_padding_set_ ? discard_padding_ : 0); + block_data_.reset(); + block_data_size_ = -1; + block_duration_ = -1; + block_add_id_ = -1; + block_additional_data_.reset(); + block_additional_data_size_ = 0; + discard_padding_ = -1; + discard_padding_set_ = false; + return result; +} + +bool WebMClusterParser::OnUInt(int id, int64 val) { + int64* dst; + switch (id) { + case kWebMIdTimecode: + dst = &cluster_timecode_; + break; + case kWebMIdBlockDuration: + dst = &block_duration_; + break; + case kWebMIdBlockAddID: + dst = &block_add_id_; + break; + default: + return true; + } + if (*dst != -1) + return false; + *dst = val; + return true; +} + +bool WebMClusterParser::ParseBlock(bool is_simple_block, + const uint8_t* buf, + int size, + const uint8_t* additional, + int additional_size, + int duration, + int64 discard_padding) { + if (size < 4) + return false; + + // Return an error if the trackNum > 127. We just aren't + // going to support large track numbers right now. + if (!(buf[0] & 0x80)) { + MEDIA_LOG(ERROR, media_log_) << "TrackNumber over 127 not supported"; + return false; + } + + int track_num = buf[0] & 0x7f; + int timecode = buf[1] << 8 | buf[2]; + int flags = buf[3] & 0xff; + int lacing = (flags >> 1) & 0x3; + + if (lacing) { + MEDIA_LOG(ERROR, media_log_) << "Lacing " << lacing + << " is not supported yet."; + return false; + } + + // Sign extend negative timecode offsets. + if (timecode & 0x8000) + timecode |= ~0xffff; + + const uint8_t* frame_data = buf + 4; + int frame_size = size - (frame_data - buf); + return OnBlock(is_simple_block, track_num, timecode, duration, flags, + frame_data, frame_size, additional, additional_size, + discard_padding); +} + +bool WebMClusterParser::OnBinary(int id, const uint8_t* data, int size) { + switch (id) { + case kWebMIdSimpleBlock: + return ParseBlock(true, data, size, NULL, 0, -1, 0); + + case kWebMIdBlock: + if (block_data_) { + MEDIA_LOG(ERROR, media_log_) + << "More than 1 Block in a BlockGroup is not " + "supported."; + return false; + } + block_data_.reset(new uint8_t[size]); + memcpy(block_data_.get(), data, size); + block_data_size_ = size; + return true; + + case kWebMIdBlockAdditional: { + uint64 block_add_id = base::HostToNet64(block_add_id_); + if (block_additional_data_) { + // TODO(vigneshv): Technically, more than 1 BlockAdditional is allowed + // as per matroska spec. But for now we don't have a use case to + // support parsing of such files. Take a look at this again when such a + // case arises. + MEDIA_LOG(ERROR, media_log_) << "More than 1 BlockAdditional in a " + "BlockGroup is not supported."; + return false; + } + // First 8 bytes of side_data in DecoderBuffer is the BlockAddID + // element's value in Big Endian format. This is done to mimic ffmpeg + // demuxer's behavior. + block_additional_data_size_ = size + sizeof(block_add_id); + block_additional_data_.reset(new uint8_t[block_additional_data_size_]); + memcpy(block_additional_data_.get(), &block_add_id, + sizeof(block_add_id)); + memcpy(block_additional_data_.get() + 8, data, size); + return true; + } + case kWebMIdDiscardPadding: { + if (discard_padding_set_ || size <= 0 || size > 8) + return false; + discard_padding_set_ = true; + + // Read in the big-endian integer. + discard_padding_ = static_cast(data[0]); + for (int i = 1; i < size; ++i) + discard_padding_ = (discard_padding_ << 8) | data[i]; + + return true; + } + default: + return true; + } +} + +bool WebMClusterParser::OnBlock(bool is_simple_block, + int track_num, + int timecode, + int block_duration, + int flags, + const uint8_t* data, + int size, + const uint8_t* additional, + int additional_size, + int64 discard_padding) { + DCHECK_GE(size, 0); + if (cluster_timecode_ == -1) { + MEDIA_LOG(ERROR, media_log_) << "Got a block before cluster timecode."; + return false; + } + + // TODO(acolwell): Should relative negative timecode offsets be rejected? Or + // only when the absolute timecode is negative? See http://crbug.com/271794 + if (timecode < 0) { + MEDIA_LOG(ERROR, media_log_) << "Got a block with negative timecode offset " + << timecode; + return false; + } + + if (last_block_timecode_ != -1 && timecode < last_block_timecode_) { + MEDIA_LOG(ERROR, media_log_) + << "Got a block with a timecode before the previous block."; + return false; + } + + Track* track = NULL; + StreamParserBuffer::Type buffer_type = DemuxerStream::AUDIO; + std::string encryption_key_id; + base::TimeDelta encoded_duration = kNoTimestamp(); + if (track_num == audio_.track_num()) { + track = &audio_; + encryption_key_id = audio_encryption_key_id_; + if (encryption_key_id.empty()) { + encoded_duration = TryGetEncodedAudioDuration(data, size); + } + } else if (track_num == video_.track_num()) { + track = &video_; + encryption_key_id = video_encryption_key_id_; + buffer_type = DemuxerStream::VIDEO; + } else if (ignored_tracks_.find(track_num) != ignored_tracks_.end()) { + return true; + } else if (Track* const text_track = FindTextTrack(track_num)) { + if (is_simple_block) // BlockGroup is required for WebVTT cues + return false; + if (block_duration < 0) // not specified + return false; + track = text_track; + buffer_type = DemuxerStream::TEXT; + } else { + MEDIA_LOG(ERROR, media_log_) << "Unexpected track number " << track_num; + return false; + } + + last_block_timecode_ = timecode; + + base::TimeDelta timestamp = base::TimeDelta::FromMicroseconds( + (cluster_timecode_ + timecode) * timecode_multiplier_); + + scoped_refptr buffer; + if (buffer_type != DemuxerStream::TEXT) { + // The first bit of the flags is set when a SimpleBlock contains only + // keyframes. If this is a Block, then inspection of the payload is + // necessary to determine whether it contains a keyframe or not. + // http://www.matroska.org/technical/specs/index.html + bool is_keyframe = + is_simple_block ? (flags & 0x80) != 0 : track->IsKeyframe(data, size); + + // Every encrypted Block has a signal byte and IV prepended to it. Current + // encrypted WebM request for comments specification is here + // http://wiki.webmproject.org/encryption/webm-encryption-rfc + scoped_ptr decrypt_config; + int data_offset = 0; + if (!encryption_key_id.empty() && + !WebMCreateDecryptConfig( + data, size, + reinterpret_cast(encryption_key_id.data()), + encryption_key_id.size(), + &decrypt_config, &data_offset)) { + return false; + } + + // TODO(wolenetz/acolwell): Validate and use a common cross-parser TrackId + // type with remapped bytestream track numbers and allow multiple tracks as + // applicable. See https://crbug.com/341581. + buffer = StreamParserBuffer::CopyFrom( + data + data_offset, size - data_offset, + additional, additional_size, + is_keyframe, buffer_type, track_num); + + if (decrypt_config) + buffer->set_decrypt_config(decrypt_config.Pass()); + } else { + std::string id, settings, content; + WebMWebVTTParser::Parse(data, size, &id, &settings, &content); + + std::vector side_data; + MakeSideData(id.begin(), id.end(), + settings.begin(), settings.end(), + &side_data); + + // TODO(wolenetz/acolwell): Validate and use a common cross-parser TrackId + // type with remapped bytestream track numbers and allow multiple tracks as + // applicable. See https://crbug.com/341581. + buffer = StreamParserBuffer::CopyFrom( + reinterpret_cast(content.data()), + content.length(), + &side_data[0], + side_data.size(), + true, buffer_type, track_num); + } + + buffer->set_timestamp(timestamp); + if (cluster_start_time_ == kNoTimestamp()) + cluster_start_time_ = timestamp; + + base::TimeDelta block_duration_time_delta = kNoTimestamp(); + if (block_duration >= 0) { + block_duration_time_delta = base::TimeDelta::FromMicroseconds( + block_duration * timecode_multiplier_); + } + + // Prefer encoded duration over BlockGroup->BlockDuration or + // TrackEntry->DefaultDuration when available. This layering violation is a + // workaround for http://crbug.com/396634, decreasing the likelihood of + // fall-back to rough estimation techniques for Blocks that lack a + // BlockDuration at the end of a cluster. Cross cluster durations are not + // feasible given flexibility of cluster ordering and MSE APIs. Duration + // estimation may still apply in cases of encryption and codecs for which + // we do not extract encoded duration. Within a cluster, estimates are applied + // as Block Timecode deltas, or once the whole cluster is parsed in the case + // of the last Block in the cluster. See Track::AddBuffer and + // ApplyDurationEstimateIfNeeded(). + if (encoded_duration != kNoTimestamp()) { + DCHECK(encoded_duration != kInfiniteDuration()); + DCHECK(encoded_duration > base::TimeDelta()); + buffer->set_duration(encoded_duration); + + DVLOG(3) << __FUNCTION__ << " : " + << "Using encoded duration " << encoded_duration.InSecondsF(); + + if (block_duration_time_delta != kNoTimestamp()) { + base::TimeDelta duration_difference = + block_duration_time_delta - encoded_duration; + + const auto kWarnDurationDiff = + base::TimeDelta::FromMicroseconds(timecode_multiplier_ * 2); + if (duration_difference.magnitude() > kWarnDurationDiff) { + LIMITED_MEDIA_LOG(DEBUG, media_log_, num_duration_errors_, + kMaxDurationErrorLogs) + << "BlockDuration (" << block_duration_time_delta.InMilliseconds() + << "ms) differs significantly from encoded duration (" + << encoded_duration.InMilliseconds() << "ms)."; + } + } + } else if (block_duration_time_delta != kNoTimestamp()) { + buffer->set_duration(block_duration_time_delta); + } else { + DCHECK_NE(buffer_type, DemuxerStream::TEXT); + buffer->set_duration(track->default_duration()); + } + + if (discard_padding != 0) { + buffer->set_discard_padding(std::make_pair( + base::TimeDelta(), + base::TimeDelta::FromMicroseconds(discard_padding / 1000))); + } + + return track->AddBuffer(buffer); +} + +WebMClusterParser::Track::Track(int track_num, + bool is_video, + base::TimeDelta default_duration, + const scoped_refptr& media_log) + : track_num_(track_num), + is_video_(is_video), + default_duration_(default_duration), + estimated_next_frame_duration_(kNoTimestamp()), + media_log_(media_log) { + DCHECK(default_duration_ == kNoTimestamp() || + default_duration_ > base::TimeDelta()); +} + +WebMClusterParser::Track::~Track() {} + +DecodeTimestamp WebMClusterParser::Track::GetReadyUpperBound() { + DCHECK(ready_buffers_.empty()); + if (last_added_buffer_missing_duration_.get()) + return last_added_buffer_missing_duration_->GetDecodeTimestamp(); + + return DecodeTimestamp::FromPresentationTime(base::TimeDelta::Max()); +} + +void WebMClusterParser::Track::ExtractReadyBuffers( + const DecodeTimestamp before_timestamp) { + DCHECK(ready_buffers_.empty()); + DCHECK(DecodeTimestamp() <= before_timestamp); + DCHECK(kNoDecodeTimestamp() != before_timestamp); + + if (buffers_.empty()) + return; + + if (buffers_.back()->GetDecodeTimestamp() < before_timestamp) { + // All of |buffers_| are ready. + ready_buffers_.swap(buffers_); + DVLOG(3) << __FUNCTION__ << " : " << track_num_ << " All " + << ready_buffers_.size() << " are ready: before upper bound ts " + << before_timestamp.InSecondsF(); + return; + } + + // Not all of |buffers_| are ready yet. Move any that are ready to + // |ready_buffers_|. + while (true) { + const scoped_refptr& buffer = buffers_.front(); + if (buffer->GetDecodeTimestamp() >= before_timestamp) + break; + ready_buffers_.push_back(buffer); + buffers_.pop_front(); + DCHECK(!buffers_.empty()); + } + + DVLOG(3) << __FUNCTION__ << " : " << track_num_ << " Only " + << ready_buffers_.size() << " ready, " << buffers_.size() + << " at or after upper bound ts " << before_timestamp.InSecondsF(); +} + +bool WebMClusterParser::Track::AddBuffer( + const scoped_refptr& buffer) { + DVLOG(2) << "AddBuffer() : " << track_num_ + << " ts " << buffer->timestamp().InSecondsF() + << " dur " << buffer->duration().InSecondsF() + << " kf " << buffer->is_key_frame() + << " size " << buffer->data_size(); + + if (last_added_buffer_missing_duration_.get()) { + base::TimeDelta derived_duration = + buffer->timestamp() - last_added_buffer_missing_duration_->timestamp(); + last_added_buffer_missing_duration_->set_duration(derived_duration); + + DVLOG(2) << "AddBuffer() : applied derived duration to held-back buffer : " + << " ts " + << last_added_buffer_missing_duration_->timestamp().InSecondsF() + << " dur " + << last_added_buffer_missing_duration_->duration().InSecondsF() + << " kf " << last_added_buffer_missing_duration_->is_key_frame() + << " size " << last_added_buffer_missing_duration_->data_size(); + scoped_refptr updated_buffer = + last_added_buffer_missing_duration_; + last_added_buffer_missing_duration_ = NULL; + if (!QueueBuffer(updated_buffer)) + return false; + } + + if (buffer->duration() == kNoTimestamp()) { + last_added_buffer_missing_duration_ = buffer; + DVLOG(2) << "AddBuffer() : holding back buffer that is missing duration"; + return true; + } + + return QueueBuffer(buffer); +} + +void WebMClusterParser::Track::ApplyDurationEstimateIfNeeded() { + if (!last_added_buffer_missing_duration_.get()) + return; + + base::TimeDelta estimated_duration = GetDurationEstimate(); + last_added_buffer_missing_duration_->set_duration(estimated_duration); + + if (is_video_) { + // Exposing estimation so splicing/overlap frame processing can make + // informed decisions downstream. + // TODO(chcunningham): Set this for audio as well in later change where + // audio is switched to max estimation and splicing is disabled. + last_added_buffer_missing_duration_->set_is_duration_estimated(true); + } + + LIMITED_MEDIA_LOG(INFO, media_log_, num_duration_estimates_, + kMaxDurationEstimateLogs) + << "Estimating WebM block duration to be " + << estimated_duration.InMilliseconds() + << "ms for the last (Simple)Block in the Cluster for this Track. Use " + "BlockGroups with BlockDurations at the end of each Track in a " + "Cluster to avoid estimation."; + + DVLOG(2) << __FUNCTION__ << " new dur : ts " + << last_added_buffer_missing_duration_->timestamp().InSecondsF() + << " dur " + << last_added_buffer_missing_duration_->duration().InSecondsF() + << " kf " << last_added_buffer_missing_duration_->is_key_frame() + << " size " << last_added_buffer_missing_duration_->data_size(); + + // Don't use the applied duration as a future estimation (don't use + // QueueBuffer() here.) + buffers_.push_back(last_added_buffer_missing_duration_); + last_added_buffer_missing_duration_ = NULL; +} + +void WebMClusterParser::Track::ClearReadyBuffers() { + // Note that |buffers_| are kept and |estimated_next_frame_duration_| is not + // reset here. + ready_buffers_.clear(); +} + +void WebMClusterParser::Track::Reset() { + ClearReadyBuffers(); + buffers_.clear(); + last_added_buffer_missing_duration_ = NULL; +} + +bool WebMClusterParser::Track::IsKeyframe(const uint8_t* data, int size) const { + // For now, assume that all blocks are keyframes for datatypes other than + // video. This is a valid assumption for Vorbis, WebVTT, & Opus. + if (!is_video_) + return true; + + // Make sure the block is big enough for the minimal keyframe header size. + if (size < 7) + return false; + + // The LSb of the first byte must be a 0 for a keyframe. + // http://tools.ietf.org/html/rfc6386 Section 19.1 + if ((data[0] & 0x01) != 0) + return false; + + // Verify VP8 keyframe startcode. + // http://tools.ietf.org/html/rfc6386 Section 19.1 + if (data[3] != 0x9d || data[4] != 0x01 || data[5] != 0x2a) + return false; + + return true; +} + +bool WebMClusterParser::Track::QueueBuffer( + const scoped_refptr& buffer) { + DCHECK(!last_added_buffer_missing_duration_.get()); + + // WebMClusterParser::OnBlock() gives MEDIA_LOG and parse error on decreasing + // block timecode detection within a cluster. Therefore, we should not see + // those here. + DecodeTimestamp previous_buffers_timestamp = buffers_.empty() ? + DecodeTimestamp() : buffers_.back()->GetDecodeTimestamp(); + CHECK(previous_buffers_timestamp <= buffer->GetDecodeTimestamp()); + + base::TimeDelta duration = buffer->duration(); + if (duration < base::TimeDelta() || duration == kNoTimestamp()) { + MEDIA_LOG(ERROR, media_log_) + << "Invalid buffer duration: " << duration.InSecondsF(); + return false; + } + + // The estimated frame duration is the minimum (for audio) or the maximum + // (for video) non-zero duration since the last initialization segment. The + // minimum is used for audio to ensure frame durations aren't overestimated, + // triggering unnecessary frame splicing. For video, splicing does not apply, + // so maximum is used and overlap is simply resolved by showing the + // later of the overlapping frames at its given PTS, effectively trimming down + // the over-estimated duration of the previous frame. + // TODO(chcunningham): Use max for audio and disable splicing whenever + // estimated buffers are encountered. + if (duration > base::TimeDelta()) { + base::TimeDelta orig_duration_estimate = estimated_next_frame_duration_; + if (estimated_next_frame_duration_ == kNoTimestamp()) { + estimated_next_frame_duration_ = duration; + } else if (is_video_) { + estimated_next_frame_duration_ = + std::max(duration, estimated_next_frame_duration_); + } else { + estimated_next_frame_duration_ = + std::min(duration, estimated_next_frame_duration_); + } + + if (orig_duration_estimate != estimated_next_frame_duration_) { + DVLOG(3) << "Updated duration estimate:" + << orig_duration_estimate + << " -> " + << estimated_next_frame_duration_ + << " at timestamp: " + << buffer->GetDecodeTimestamp().InSecondsF(); + } + } + + buffers_.push_back(buffer); + return true; +} + +base::TimeDelta WebMClusterParser::Track::GetDurationEstimate() { + base::TimeDelta duration = estimated_next_frame_duration_; + if (duration != kNoTimestamp()) { + DVLOG(3) << __FUNCTION__ << " : using estimated duration"; + } else { + DVLOG(3) << __FUNCTION__ << " : using hardcoded default duration"; + if (is_video_) { + duration = base::TimeDelta::FromMilliseconds( + kDefaultVideoBufferDurationInMs); + } else { + duration = base::TimeDelta::FromMilliseconds( + kDefaultAudioBufferDurationInMs); + } + } + + DCHECK(duration > base::TimeDelta()); + DCHECK(duration != kNoTimestamp()); + return duration; +} + +void WebMClusterParser::ClearTextTrackReadyBuffers() { + text_buffers_map_.clear(); + for (TextTrackMap::iterator it = text_track_map_.begin(); + it != text_track_map_.end(); + ++it) { + it->second.ClearReadyBuffers(); + } +} + +void WebMClusterParser::ResetTextTracks() { + ClearTextTrackReadyBuffers(); + for (TextTrackMap::iterator it = text_track_map_.begin(); + it != text_track_map_.end(); + ++it) { + it->second.Reset(); + } +} + +void WebMClusterParser::UpdateReadyBuffers() { + DCHECK(ready_buffer_upper_bound_ == kNoDecodeTimestamp()); + DCHECK(text_buffers_map_.empty()); + + if (cluster_ended_) { + audio_.ApplyDurationEstimateIfNeeded(); + video_.ApplyDurationEstimateIfNeeded(); + // Per OnBlock(), all text buffers should already have valid durations, so + // there is no need to call ApplyDurationEstimateIfNeeded() on text tracks + // here. + ready_buffer_upper_bound_ = + DecodeTimestamp::FromPresentationTime(base::TimeDelta::Max()); + DCHECK(ready_buffer_upper_bound_ == audio_.GetReadyUpperBound()); + DCHECK(ready_buffer_upper_bound_ == video_.GetReadyUpperBound()); + } else { + ready_buffer_upper_bound_ = std::min(audio_.GetReadyUpperBound(), + video_.GetReadyUpperBound()); + DCHECK(DecodeTimestamp() <= ready_buffer_upper_bound_); + DCHECK(kNoDecodeTimestamp() != ready_buffer_upper_bound_); + } + + // Prepare each track's ready buffers for retrieval. + audio_.ExtractReadyBuffers(ready_buffer_upper_bound_); + video_.ExtractReadyBuffers(ready_buffer_upper_bound_); + for (TextTrackMap::iterator itr = text_track_map_.begin(); + itr != text_track_map_.end(); + ++itr) { + itr->second.ExtractReadyBuffers(ready_buffer_upper_bound_); + } +} + +WebMClusterParser::Track* +WebMClusterParser::FindTextTrack(int track_num) { + const TextTrackMap::iterator it = text_track_map_.find(track_num); + + if (it == text_track_map_.end()) + return NULL; + + return &it->second; +} + +} // namespace media diff --git a/packager/media/formats/webm/webm_cluster_parser.h b/packager/media/formats/webm/webm_cluster_parser.h new file mode 100644 index 0000000000..e2d5b98e6d --- /dev/null +++ b/packager/media/formats/webm/webm_cluster_parser.h @@ -0,0 +1,325 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef MEDIA_FORMATS_WEBM_WEBM_CLUSTER_PARSER_H_ +#define MEDIA_FORMATS_WEBM_WEBM_CLUSTER_PARSER_H_ + +#include +#include +#include +#include + +#include "base/memory/scoped_ptr.h" +#include "media/base/audio_decoder_config.h" +#include "media/base/media_export.h" +#include "media/base/media_log.h" +#include "media/base/stream_parser.h" +#include "media/base/stream_parser_buffer.h" +#include "media/formats/webm/webm_parser.h" +#include "media/formats/webm/webm_tracks_parser.h" + +namespace media { + +class MEDIA_EXPORT WebMClusterParser : public WebMParserClient { + public: + typedef StreamParser::TrackId TrackId; + typedef std::deque > BufferQueue; + typedef std::map TextBufferQueueMap; + + // Numbers chosen to estimate the duration of a buffer if none is set and + // there is not enough information to get a better estimate. + enum { + // Common 1k samples @44.1kHz + kDefaultAudioBufferDurationInMs = 23, + + // Chosen to represent 16fps duration, which will prevent MSE stalls in + // videos with frame-rates as low as 8fps. + kDefaultVideoBufferDurationInMs = 63 + }; + + // Opus packets encode the duration and other parameters in the 5 most + // significant bits of the first byte. The index in this array corresponds + // to the duration of each frame of the packet in microseconds. See + // https://tools.ietf.org/html/rfc6716#page-14 + static const uint16_t kOpusFrameDurationsMu[]; + + private: + // Helper class that manages per-track state. + class Track { + public: + Track(int track_num, + bool is_video, + base::TimeDelta default_duration, + const scoped_refptr& media_log); + ~Track(); + + int track_num() const { return track_num_; } + + // If a buffer is currently held aside pending duration calculation, returns + // its decode timestamp. Otherwise, returns kInfiniteDuration(). + DecodeTimestamp GetReadyUpperBound(); + + // Prepares |ready_buffers_| for retrieval. Prior to calling, + // |ready_buffers_| must be empty. Moves all |buffers_| with decode + // timestamp before |before_timestamp| to |ready_buffers_|, preserving their + // order. + void ExtractReadyBuffers(const DecodeTimestamp before_timestamp); + + const BufferQueue& ready_buffers() const { return ready_buffers_; } + + // If |last_added_buffer_missing_duration_| is set, updates its duration + // relative to |buffer|'s timestamp, and adds it to |buffers_| and unsets + // |last_added_buffer_missing_duration_|. Then, if |buffer| is missing + // duration, saves |buffer| into |last_added_buffer_missing_duration_|, or + // otherwise adds |buffer| to |buffers_|. + bool AddBuffer(const scoped_refptr& buffer); + + // If |last_added_buffer_missing_duration_| is set, updates its duration to + // be non-kNoTimestamp() value of |estimated_next_frame_duration_| or a + // hard-coded default, then adds it to |buffers_| and unsets + // |last_added_buffer_missing_duration_|. (This method helps stream parser + // emit all buffers in a media segment before signaling end of segment.) + void ApplyDurationEstimateIfNeeded(); + + // Clears |ready_buffers_| (use ExtractReadyBuffers() to fill it again). + // Leaves as-is |buffers_| and any possibly held-aside buffer that is + // missing duration. + void ClearReadyBuffers(); + + // Clears all buffer state, including any possibly held-aside buffer that + // was missing duration, and all contents of |buffers_| and + // |ready_buffers_|. + void Reset(); + + // Helper function used to inspect block data to determine if the + // block is a keyframe. + // |data| contains the bytes in the block. + // |size| indicates the number of bytes in |data|. + bool IsKeyframe(const uint8_t* data, int size) const; + + base::TimeDelta default_duration() const { return default_duration_; } + + private: + // Helper that sanity-checks |buffer| duration, updates + // |estimated_next_frame_duration_|, and adds |buffer| to |buffers_|. + // Returns false if |buffer| failed sanity check and therefore was not added + // to |buffers_|. Returns true otherwise. + bool QueueBuffer(const scoped_refptr& buffer); + + // Helper that calculates the buffer duration to use in + // ApplyDurationEstimateIfNeeded(). + base::TimeDelta GetDurationEstimate(); + + // Counts the number of estimated durations used in this track. Used to + // prevent log spam for MEDIA_LOG()s about estimated duration. + int num_duration_estimates_ = 0; + + int track_num_; + bool is_video_; + + // Parsed track buffers, each with duration and in (decode) timestamp order, + // that have not yet been extracted into |ready_buffers_|. Note that up to + // one additional buffer missing duration may be tracked by + // |last_added_buffer_missing_duration_|. + BufferQueue buffers_; + scoped_refptr last_added_buffer_missing_duration_; + + // Buffers in (decode) timestamp order that were previously parsed into and + // extracted from |buffers_|. Buffers are moved from |buffers_| to + // |ready_buffers_| by ExtractReadyBuffers() if they are below a specified + // upper bound timestamp. Track users can therefore extract only those + // parsed buffers which are "ready" for emission (all before some maximum + // timestamp). + BufferQueue ready_buffers_; + + // If kNoTimestamp(), then |estimated_next_frame_duration_| will be used. + base::TimeDelta default_duration_; + + // If kNoTimestamp(), then a default value will be used. This estimate is + // the maximum (for video), or minimum (for audio) duration seen so far for + // this track, and is used only if |default_duration_| is kNoTimestamp(). + // TODO(chcunningham): Use maximum for audio too, adding checks to disable + // splicing when these estimates are observed in SourceBufferStream. + base::TimeDelta estimated_next_frame_duration_; + + scoped_refptr media_log_; + }; + + typedef std::map TextTrackMap; + + public: + WebMClusterParser(int64 timecode_scale, + int audio_track_num, + base::TimeDelta audio_default_duration, + int video_track_num, + base::TimeDelta video_default_duration, + const WebMTracksParser::TextTracks& text_tracks, + const std::set& ignored_tracks, + const std::string& audio_encryption_key_id, + const std::string& video_encryption_key_id, + const AudioCodec audio_codec, + const scoped_refptr& media_log); + ~WebMClusterParser() override; + + // Resets the parser state so it can accept a new cluster. + void Reset(); + + // Parses a WebM cluster element in |buf|. + // + // Returns -1 if the parse fails. + // Returns 0 if more data is needed. + // Returns the number of bytes parsed on success. + int Parse(const uint8_t* buf, int size); + + base::TimeDelta cluster_start_time() const { return cluster_start_time_; } + + // Get the current ready buffers resulting from Parse(). + // If the parse reached the end of cluster and the last buffer was held aside + // due to missing duration, the buffer is given an estimated duration and + // included in the result. + // Otherwise, if there are is a buffer held aside due to missing duration for + // any of the tracks, no buffers with same or greater (decode) timestamp will + // be included in the buffers. + // The returned deques are cleared by Parse() or Reset() and updated by the + // next calls to Get{Audio,Video}Buffers(). + // If no Parse() or Reset() has occurred since the last call to Get{Audio, + // Video,Text}Buffers(), then the previous BufferQueue& is returned again + // without any recalculation. + const BufferQueue& GetAudioBuffers(); + const BufferQueue& GetVideoBuffers(); + + // Constructs and returns a subset of |text_track_map_| containing only + // tracks with non-empty buffer queues produced by the last Parse() and + // filtered to exclude any buffers that have (decode) timestamp same or + // greater than the lowest (decode) timestamp across all tracks of any buffer + // held aside due to missing duration (unless the end of cluster has been + // reached). + // The returned map is cleared by Parse() or Reset() and updated by the next + // call to GetTextBuffers(). + // If no Parse() or Reset() has occurred since the last call to + // GetTextBuffers(), then the previous TextBufferQueueMap& is returned again + // without any recalculation. + const TextBufferQueueMap& GetTextBuffers(); + + // Returns true if the last Parse() call stopped at the end of a cluster. + bool cluster_ended() const { return cluster_ended_; } + + private: + // WebMParserClient methods. + WebMParserClient* OnListStart(int id) override; + bool OnListEnd(int id) override; + bool OnUInt(int id, int64 val) override; + bool OnBinary(int id, const uint8_t* data, int size) override; + + bool ParseBlock(bool is_simple_block, + const uint8_t* buf, + int size, + const uint8_t* additional, + int additional_size, + int duration, + int64 discard_padding); + bool OnBlock(bool is_simple_block, + int track_num, + int timecode, + int duration, + int flags, + const uint8_t* data, + int size, + const uint8_t* additional, + int additional_size, + int64 discard_padding); + + // Resets the Track objects associated with each text track. + void ResetTextTracks(); + + // Clears the the ready buffers associated with each text track. + void ClearTextTrackReadyBuffers(); + + // Helper method for Get{Audio,Video,Text}Buffers() that recomputes + // |ready_buffer_upper_bound_| and calls ExtractReadyBuffers() on each track. + // If |cluster_ended_| is true, first applies duration estimate if needed for + // |audio_| and |video_| and sets |ready_buffer_upper_bound_| to + // kInfiniteDuration(). Otherwise, sets |ready_buffer_upper_bound_| to the + // minimum upper bound across |audio_| and |video_|. (Text tracks can have no + // buffers missing duration, so they are not involved in calculating the upper + // bound.) + // Parse() or Reset() must be called between calls to UpdateReadyBuffers() to + // clear each track's ready buffers and to reset |ready_buffer_upper_bound_| + // to kNoDecodeTimestamp(). + void UpdateReadyBuffers(); + + // Search for the indicated track_num among the text tracks. Returns NULL + // if that track num is not a text track. + Track* FindTextTrack(int track_num); + + // Attempts to read the duration from the encoded audio data, returning as + // TimeDelta or kNoTimestamp() if duration cannot be retrieved. This obviously + // violates layering rules, but is useful for MSE to know duration in cases + // where it isn't explicitly given and cannot be calculated for Blocks at the + // end of a Cluster (the next Cluster in playback-order may not be the next + // Cluster we parse, so we can't simply use the delta of the first Block in + // the next Cluster). Avoid calling if encrypted; may produce unexpected + // output. See implementation for supported codecs. + base::TimeDelta TryGetEncodedAudioDuration(const uint8_t* data, int size); + + // Reads Opus packet header to determine packet duration. Duration returned + // as TimeDelta or kNoTimestamp() upon failure to read duration from packet. + base::TimeDelta ReadOpusDuration(const uint8_t* data, int size); + + // Tracks the number of MEDIA_LOGs made in process of reading encoded + // duration. Useful to prevent log spam. + int num_duration_errors_ = 0; + + double timecode_multiplier_; // Multiplier used to convert timecodes into + // microseconds. + std::set ignored_tracks_; + std::string audio_encryption_key_id_; + std::string video_encryption_key_id_; + const AudioCodec audio_codec_; + + WebMListParser parser_; + + int64 last_block_timecode_ = -1; + scoped_ptr block_data_; + int block_data_size_ = -1; + int64 block_duration_ = -1; + int64 block_add_id_ = -1; + + scoped_ptr block_additional_data_; + // Must be 0 if |block_additional_data_| is null. Must be > 0 if + // |block_additional_data_| is NOT null. + int block_additional_data_size_ = 0; + + int64 discard_padding_ = -1; + bool discard_padding_set_ = false; + + int64 cluster_timecode_ = -1; + base::TimeDelta cluster_start_time_; + bool cluster_ended_ = false; + + Track audio_; + Track video_; + TextTrackMap text_track_map_; + + // Subset of |text_track_map_| maintained by GetTextBuffers(), and cleared by + // ClearTextTrackReadyBuffers(). Callers of GetTextBuffers() get a const-ref + // to this member. + TextBufferQueueMap text_buffers_map_; + + // Limits the range of buffers returned by Get{Audio,Video,Text}Buffers() to + // this exclusive upper bound. Set to kNoDecodeTimestamp(), meaning not yet + // calculated, by Reset() and Parse(). If kNoDecodeTimestamp(), then + // Get{Audio,Video,Text}Buffers() will calculate it to be the minimum (decode) + // timestamp across all tracks' |last_buffer_missing_duration_|, or + // kInfiniteDuration() if no buffers are currently missing duration. + DecodeTimestamp ready_buffer_upper_bound_; + + scoped_refptr media_log_; + + DISALLOW_IMPLICIT_CONSTRUCTORS(WebMClusterParser); +}; + +} // namespace media + +#endif // MEDIA_FORMATS_WEBM_WEBM_CLUSTER_PARSER_H_ diff --git a/packager/media/formats/webm/webm_cluster_parser_unittest.cc b/packager/media/formats/webm/webm_cluster_parser_unittest.cc new file mode 100644 index 0000000000..7581bbf866 --- /dev/null +++ b/packager/media/formats/webm/webm_cluster_parser_unittest.cc @@ -0,0 +1,1152 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include +#include +#include + +#include "base/bind.h" +#include "base/logging.h" +#include "base/strings/string_number_conversions.h" +#include "media/base/audio_decoder_config.h" +#include "media/base/decrypt_config.h" +#include "media/base/mock_media_log.h" +#include "media/base/timestamp_constants.h" +#include "media/formats/webm/cluster_builder.h" +#include "media/formats/webm/opus_packet_builder.h" +#include "media/formats/webm/webm_cluster_parser.h" +#include "media/formats/webm/webm_constants.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using ::testing::HasSubstr; +using ::testing::InSequence; +using ::testing::Return; +using ::testing::StrictMock; +using ::testing::Mock; +using ::testing::_; + +namespace media { + +typedef WebMTracksParser::TextTracks TextTracks; + +// Matchers for verifying common media log entry strings. +MATCHER_P(OpusPacketDurationTooHigh, actual_duration_ms, "") { + return CONTAINS_STRING( + arg, "Warning, demuxed Opus packet with encoded duration: " + + base::IntToString(actual_duration_ms) + + "ms. Should be no greater than 120ms."); +} + +MATCHER_P(WebMSimpleBlockDurationEstimated, estimated_duration_ms, "") { + return CONTAINS_STRING(arg, "Estimating WebM block duration to be " + + base::IntToString(estimated_duration_ms) + + "ms for the last (Simple)Block in the " + "Cluster for this Track. Use BlockGroups " + "with BlockDurations at the end of each " + "Track in a Cluster to avoid estimation."); +} + +MATCHER_P2(WebMBlockDurationMismatchesOpusDuration, + block_duration_ms, + opus_duration_ms, + "") { + return CONTAINS_STRING( + arg, "BlockDuration (" + base::IntToString(block_duration_ms) + + "ms) differs significantly from encoded duration (" + + base::IntToString(opus_duration_ms) + "ms)."); +} + +namespace { + +// Timecode scale for millisecond timestamps. +const int kTimecodeScale = 1000000; + +const int kAudioTrackNum = 1; +const int kVideoTrackNum = 2; +const int kTextTrackNum = 3; +const int kTestAudioFrameDefaultDurationInMs = 13; +const int kTestVideoFrameDefaultDurationInMs = 17; + +// Test duration defaults must differ from parser estimation defaults to know +// which durations parser used when emitting buffers. +static_assert( + static_cast(kTestAudioFrameDefaultDurationInMs) != + static_cast(WebMClusterParser::kDefaultAudioBufferDurationInMs), + "test default is the same as estimation fallback audio duration"); +static_assert( + static_cast(kTestVideoFrameDefaultDurationInMs) != + static_cast(WebMClusterParser::kDefaultVideoBufferDurationInMs), + "test default is the same as estimation fallback video duration"); + +struct BlockInfo { + int track_num; + int timestamp; + + // Negative value is allowed only for block groups (not simple blocks) and + // directs CreateCluster() to exclude BlockDuration entry from the cluster for + // this BlockGroup. The absolute value is used for parser verification. + // For simple blocks, this value must be non-negative, and is used only for + // parser verification. + double duration; + + bool use_simple_block; + + // Default data will be used if no data given. + const uint8_t* data; + int data_length; +}; + +const BlockInfo kDefaultBlockInfo[] = { + {kAudioTrackNum, 0, 23, true, NULL, 0}, + {kAudioTrackNum, 23, 23, true, NULL, 0}, + // Assumes not using DefaultDuration + {kVideoTrackNum, 33, 34, true, NULL, 0}, + {kAudioTrackNum, 46, 23, true, NULL, 0}, + {kVideoTrackNum, 67, 33, false, NULL, 0}, + {kAudioTrackNum, 69, 23, false, NULL, 0}, + {kVideoTrackNum, 100, 33, false, NULL, 0}, +}; + +const uint8_t kEncryptedFrame[] = { + // Block is encrypted + 0x01, + + // IV + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; + +scoped_ptr CreateCluster(int timecode, + const BlockInfo* block_info, + int block_count) { + ClusterBuilder cb; + cb.SetClusterTimecode(0); + + uint8_t kDefaultBlockData[] = { 0x00 }; + + for (int i = 0; i < block_count; i++) { + const uint8_t* data; + int data_length; + if (block_info[i].data != NULL) { + data = block_info[i].data; + data_length = block_info[i].data_length; + } else { + data = kDefaultBlockData; + data_length = sizeof(kDefaultBlockData); + } + + if (block_info[i].use_simple_block) { + CHECK_GE(block_info[i].duration, 0); + cb.AddSimpleBlock(block_info[i].track_num, block_info[i].timestamp, 0, + data, data_length); + continue; + } + + if (block_info[i].duration < 0) { + cb.AddBlockGroupWithoutBlockDuration(block_info[i].track_num, + block_info[i].timestamp, 0, data, + data_length); + continue; + } + + cb.AddBlockGroup(block_info[i].track_num, block_info[i].timestamp, + block_info[i].duration, 0, data, data_length); + } + + return cb.Finish(); +} + +// Creates a Cluster with one encrypted Block. |bytes_to_write| is number of +// bytes of the encrypted frame to write. +scoped_ptr CreateEncryptedCluster(int bytes_to_write) { + CHECK_GT(bytes_to_write, 0); + CHECK_LE(bytes_to_write, static_cast(sizeof(kEncryptedFrame))); + + ClusterBuilder cb; + cb.SetClusterTimecode(0); + cb.AddSimpleBlock(kVideoTrackNum, 0, 0, kEncryptedFrame, bytes_to_write); + return cb.Finish(); +} + +bool VerifyBuffers(const WebMClusterParser::BufferQueue& audio_buffers, + const WebMClusterParser::BufferQueue& video_buffers, + const WebMClusterParser::BufferQueue& text_buffers, + const BlockInfo* block_info, + int block_count) { + int buffer_count = audio_buffers.size() + video_buffers.size() + + text_buffers.size(); + if (block_count != buffer_count) { + DVLOG(1) << __FUNCTION__ << " : block_count (" << block_count + << ") mismatches buffer_count (" << buffer_count << ")"; + return false; + } + + size_t audio_offset = 0; + size_t video_offset = 0; + size_t text_offset = 0; + for (int i = 0; i < block_count; i++) { + const WebMClusterParser::BufferQueue* buffers = NULL; + size_t* offset; + StreamParserBuffer::Type expected_type = DemuxerStream::UNKNOWN; + + if (block_info[i].track_num == kAudioTrackNum) { + buffers = &audio_buffers; + offset = &audio_offset; + expected_type = DemuxerStream::AUDIO; + } else if (block_info[i].track_num == kVideoTrackNum) { + buffers = &video_buffers; + offset = &video_offset; + expected_type = DemuxerStream::VIDEO; + } else if (block_info[i].track_num == kTextTrackNum) { + buffers = &text_buffers; + offset = &text_offset; + expected_type = DemuxerStream::TEXT; + } else { + LOG(ERROR) << "Unexpected track number " << block_info[i].track_num; + return false; + } + + if (*offset >= buffers->size()) { + DVLOG(1) << __FUNCTION__ << " : Too few buffers (" << buffers->size() + << ") for track_num (" << block_info[i].track_num + << "), expected at least " << *offset + 1 << " buffers"; + return false; + } + + scoped_refptr buffer = (*buffers)[(*offset)++]; + + EXPECT_EQ(block_info[i].timestamp, buffer->timestamp().InMilliseconds()); + EXPECT_EQ(std::abs(block_info[i].duration), + buffer->duration().InMillisecondsF()); + EXPECT_EQ(expected_type, buffer->type()); + EXPECT_EQ(block_info[i].track_num, buffer->track_id()); + } + + return true; +} + +bool VerifyBuffers(const scoped_ptr& parser, + const BlockInfo* block_info, + int block_count) { + const WebMClusterParser::TextBufferQueueMap& text_map = + parser->GetTextBuffers(); + const WebMClusterParser::BufferQueue* text_buffers; + const WebMClusterParser::BufferQueue no_text_buffers; + if (!text_map.empty()) + text_buffers = &(text_map.rbegin()->second); + else + text_buffers = &no_text_buffers; + + return VerifyBuffers(parser->GetAudioBuffers(), + parser->GetVideoBuffers(), + *text_buffers, + block_info, + block_count); +} + +bool VerifyTextBuffers(const scoped_ptr& parser, + const BlockInfo* block_info_ptr, + int block_count, + int text_track_num, + const WebMClusterParser::BufferQueue& text_buffers) { + const BlockInfo* const block_info_end = block_info_ptr + block_count; + + typedef WebMClusterParser::BufferQueue::const_iterator TextBufferIter; + TextBufferIter buffer_iter = text_buffers.begin(); + const TextBufferIter buffer_end = text_buffers.end(); + + while (block_info_ptr != block_info_end) { + const BlockInfo& block_info = *block_info_ptr++; + + if (block_info.track_num != text_track_num) + continue; + + EXPECT_FALSE(block_info.use_simple_block); + EXPECT_FALSE(buffer_iter == buffer_end); + + const scoped_refptr buffer = *buffer_iter++; + EXPECT_EQ(block_info.timestamp, buffer->timestamp().InMilliseconds()); + EXPECT_EQ(std::abs(block_info.duration), + buffer->duration().InMillisecondsF()); + EXPECT_EQ(DemuxerStream::TEXT, buffer->type()); + EXPECT_EQ(text_track_num, buffer->track_id()); + } + + EXPECT_TRUE(buffer_iter == buffer_end); + return true; +} + +void VerifyEncryptedBuffer(scoped_refptr buffer) { + EXPECT_TRUE(buffer->decrypt_config()); + EXPECT_EQ(static_cast(DecryptConfig::kDecryptionKeySize), + buffer->decrypt_config()->iv().length()); +} + +void AppendToEnd(const WebMClusterParser::BufferQueue& src, + WebMClusterParser::BufferQueue* dest) { + for (WebMClusterParser::BufferQueue::const_iterator itr = src.begin(); + itr != src.end(); ++itr) { + dest->push_back(*itr); + } +} + +} // namespace + +class WebMClusterParserTest : public testing::Test { + public: + WebMClusterParserTest() + : media_log_(new StrictMock()), + parser_(CreateDefaultParser()) {} + + protected: + void ResetParserToHaveDefaultDurations() { + base::TimeDelta default_audio_duration = base::TimeDelta::FromMilliseconds( + kTestAudioFrameDefaultDurationInMs); + base::TimeDelta default_video_duration = base::TimeDelta::FromMilliseconds( + kTestVideoFrameDefaultDurationInMs); + ASSERT_GE(default_audio_duration, base::TimeDelta()); + ASSERT_GE(default_video_duration, base::TimeDelta()); + ASSERT_NE(kNoTimestamp(), default_audio_duration); + ASSERT_NE(kNoTimestamp(), default_video_duration); + + parser_.reset(CreateParserWithDefaultDurationsAndOptionalTextTracks( + default_audio_duration, default_video_duration)); + } + + // Helper that hard-codes some non-varying constructor parameters. + WebMClusterParser* CreateParserHelper( + base::TimeDelta audio_default_duration, + base::TimeDelta video_default_duration, + const WebMTracksParser::TextTracks& text_tracks, + const std::set& ignored_tracks, + const std::string& audio_encryption_key_id, + const std::string& video_encryption_key_id, + const AudioCodec audio_codec) { + return new WebMClusterParser( + kTimecodeScale, kAudioTrackNum, audio_default_duration, kVideoTrackNum, + video_default_duration, text_tracks, ignored_tracks, + audio_encryption_key_id, video_encryption_key_id, audio_codec, + media_log_); + } + + // Create a default version of the parser for test. + WebMClusterParser* CreateDefaultParser() { + return CreateParserHelper(kNoTimestamp(), kNoTimestamp(), TextTracks(), + std::set(), std::string(), std::string(), + kUnknownAudioCodec); + } + + // Create a parser for test with custom audio and video default durations, and + // optionally custom text tracks. + WebMClusterParser* CreateParserWithDefaultDurationsAndOptionalTextTracks( + base::TimeDelta audio_default_duration, + base::TimeDelta video_default_duration, + const WebMTracksParser::TextTracks& text_tracks = TextTracks()) { + return CreateParserHelper(audio_default_duration, video_default_duration, + text_tracks, std::set(), std::string(), + std::string(), kUnknownAudioCodec); + } + + // Create a parser for test with custom ignored tracks. + WebMClusterParser* CreateParserWithIgnoredTracks( + std::set& ignored_tracks) { + return CreateParserHelper(kNoTimestamp(), kNoTimestamp(), TextTracks(), + ignored_tracks, std::string(), std::string(), + kUnknownAudioCodec); + } + + // Create a parser for test with custom encryption key ids and audio codec. + WebMClusterParser* CreateParserWithKeyIdsAndAudioCodec( + const std::string& audio_encryption_key_id, + const std::string& video_encryption_key_id, + const AudioCodec audio_codec) { + return CreateParserHelper(kNoTimestamp(), kNoTimestamp(), TextTracks(), + std::set(), audio_encryption_key_id, + video_encryption_key_id, audio_codec); + } + + scoped_refptr> media_log_; + scoped_ptr parser_; + + private: + DISALLOW_COPY_AND_ASSIGN(WebMClusterParserTest); +}; + +TEST_F(WebMClusterParserTest, HeldBackBufferHoldsBackAllTracks) { + // If a buffer is missing duration and is being held back, then all other + // tracks' buffers that have same or higher (decode) timestamp should be held + // back too to keep the timestamps emitted for a cluster monotonically + // non-decreasing and in same order as parsed. + InSequence s; + + // Reset the parser to have 3 tracks: text, video (no default frame duration), + // and audio (with a default frame duration). + TextTracks text_tracks; + text_tracks.insert(std::make_pair(TextTracks::key_type(kTextTrackNum), + TextTrackConfig(kTextSubtitles, "", "", + ""))); + base::TimeDelta default_audio_duration = + base::TimeDelta::FromMilliseconds(kTestAudioFrameDefaultDurationInMs); + ASSERT_GE(default_audio_duration, base::TimeDelta()); + ASSERT_NE(kNoTimestamp(), default_audio_duration); + parser_.reset(CreateParserWithDefaultDurationsAndOptionalTextTracks( + default_audio_duration, kNoTimestamp(), text_tracks)); + + const int kExpectedVideoEstimationInMs = 33; + + const BlockInfo kBlockInfo[] = { + {kVideoTrackNum, 0, 33, true, NULL, 0}, + {kAudioTrackNum, 0, 23, false, NULL, 0}, + {kTextTrackNum, 10, 42, false, NULL, 0}, + {kAudioTrackNum, 23, kTestAudioFrameDefaultDurationInMs, true, NULL, 0}, + {kVideoTrackNum, 33, 33, true, NULL, 0}, + {kAudioTrackNum, 36, kTestAudioFrameDefaultDurationInMs, true, NULL, 0}, + {kVideoTrackNum, 66, kExpectedVideoEstimationInMs, true, NULL, 0}, + {kAudioTrackNum, 70, kTestAudioFrameDefaultDurationInMs, true, NULL, 0}, + {kAudioTrackNum, 83, kTestAudioFrameDefaultDurationInMs, true, NULL, 0}, + }; + + const int kExpectedBuffersOnPartialCluster[] = { + 0, // Video simple block without DefaultDuration should be held back + 0, // Audio buffer ready, but not emitted because its TS >= held back video + 0, // Text buffer ready, but not emitted because its TS >= held back video + 0, // 2nd audio buffer ready, also not emitted for same reason as first + 4, // All previous buffers emitted, 2nd video held back with no duration + 4, // 2nd video still has no duration, 3rd audio ready but not emitted + 6, // All previous buffers emitted, 3rd video held back with no duration + 6, // 3rd video still has no duration, 4th audio ready but not emitted + 9, // Cluster end emits all buffers and 3rd video's duration is estimated + }; + + ASSERT_EQ(arraysize(kBlockInfo), arraysize(kExpectedBuffersOnPartialCluster)); + int block_count = arraysize(kBlockInfo); + + // Iteratively create a cluster containing the first N+1 blocks and parse all + // but the last byte of the cluster (except when N==|block_count|, just parse + // the whole cluster). Verify that the corresponding entry in + // |kExpectedBuffersOnPartialCluster| identifies the exact subset of + // |kBlockInfo| returned by the parser. + for (int i = 0; i < block_count; ++i) { + if (i > 0) + parser_->Reset(); + // Since we don't know exactly the offsets of each block in the full + // cluster, build a cluster with exactly one additional block so that + // parse of all but one byte should deterministically parse all but the + // last full block. Don't |exceed block_count| blocks though. + int blocks_in_cluster = std::min(i + 2, block_count); + scoped_ptr cluster(CreateCluster(0, kBlockInfo, + blocks_in_cluster)); + // Parse all but the last byte unless we need to parse the full cluster. + bool parse_full_cluster = i == (block_count - 1); + + if (parse_full_cluster) { + EXPECT_MEDIA_LOG( + WebMSimpleBlockDurationEstimated(kExpectedVideoEstimationInMs)); + } + + int result = parser_->Parse(cluster->data(), parse_full_cluster ? + cluster->size() : cluster->size() - 1); + if (parse_full_cluster) { + DVLOG(1) << "Verifying parse result of full cluster of " + << blocks_in_cluster << " blocks"; + EXPECT_EQ(cluster->size(), result); + } else { + DVLOG(1) << "Verifying parse result of cluster of " + << blocks_in_cluster << " blocks with last block incomplete"; + EXPECT_GT(cluster->size(), result); + EXPECT_LT(0, result); + } + + EXPECT_TRUE(VerifyBuffers(parser_, kBlockInfo, + kExpectedBuffersOnPartialCluster[i])); + } +} + +TEST_F(WebMClusterParserTest, Reset) { + InSequence s; + + int block_count = arraysize(kDefaultBlockInfo); + scoped_ptr cluster(CreateCluster(0, kDefaultBlockInfo, block_count)); + + // Send slightly less than the full cluster so all but the last block is + // parsed. + int result = parser_->Parse(cluster->data(), cluster->size() - 1); + EXPECT_GT(result, 0); + EXPECT_LT(result, cluster->size()); + + ASSERT_TRUE(VerifyBuffers(parser_, kDefaultBlockInfo, block_count - 1)); + parser_->Reset(); + + // Now parse a whole cluster to verify that all the blocks will get parsed. + result = parser_->Parse(cluster->data(), cluster->size()); + EXPECT_EQ(cluster->size(), result); + ASSERT_TRUE(VerifyBuffers(parser_, kDefaultBlockInfo, block_count)); +} + +TEST_F(WebMClusterParserTest, ParseClusterWithSingleCall) { + int block_count = arraysize(kDefaultBlockInfo); + scoped_ptr cluster(CreateCluster(0, kDefaultBlockInfo, block_count)); + + int result = parser_->Parse(cluster->data(), cluster->size()); + EXPECT_EQ(cluster->size(), result); + ASSERT_TRUE(VerifyBuffers(parser_, kDefaultBlockInfo, block_count)); +} + +TEST_F(WebMClusterParserTest, ParseClusterWithMultipleCalls) { + int block_count = arraysize(kDefaultBlockInfo); + scoped_ptr cluster(CreateCluster(0, kDefaultBlockInfo, block_count)); + + WebMClusterParser::BufferQueue audio_buffers; + WebMClusterParser::BufferQueue video_buffers; + const WebMClusterParser::BufferQueue no_text_buffers; + + const uint8_t* data = cluster->data(); + int size = cluster->size(); + int default_parse_size = 3; + int parse_size = std::min(default_parse_size, size); + + while (size > 0) { + int result = parser_->Parse(data, parse_size); + ASSERT_GE(result, 0); + ASSERT_LE(result, parse_size); + + if (result == 0) { + // The parser needs more data so increase the parse_size a little. + parse_size += default_parse_size; + parse_size = std::min(parse_size, size); + continue; + } + + AppendToEnd(parser_->GetAudioBuffers(), &audio_buffers); + AppendToEnd(parser_->GetVideoBuffers(), &video_buffers); + + parse_size = default_parse_size; + + data += result; + size -= result; + } + ASSERT_TRUE(VerifyBuffers(audio_buffers, video_buffers, + no_text_buffers, kDefaultBlockInfo, + block_count)); +} + +// Verify that both BlockGroups with the BlockDuration before the Block +// and BlockGroups with the BlockDuration after the Block are supported +// correctly. +// Note: Raw bytes are use here because ClusterBuilder only generates +// one of these scenarios. +TEST_F(WebMClusterParserTest, ParseBlockGroup) { + const BlockInfo kBlockInfo[] = { + {kAudioTrackNum, 0, 23, false, NULL, 0}, + {kVideoTrackNum, 33, 34, false, NULL, 0}, + }; + int block_count = arraysize(kBlockInfo); + + const uint8_t kClusterData[] = { + 0x1F, 0x43, 0xB6, 0x75, 0x9B, // Cluster(size=27) + 0xE7, 0x81, 0x00, // Timecode(size=1, value=0) + // BlockGroup with BlockDuration before Block. + 0xA0, 0x8A, // BlockGroup(size=10) + 0x9B, 0x81, 0x17, // BlockDuration(size=1, value=23) + 0xA1, 0x85, 0x81, 0x00, 0x00, 0x00, 0xaa, // Block(size=5, track=1, ts=0) + // BlockGroup with BlockDuration after Block. + 0xA0, 0x8A, // BlockGroup(size=10) + 0xA1, 0x85, 0x82, 0x00, 0x21, 0x00, 0x55, // Block(size=5, track=2, ts=33) + 0x9B, 0x81, 0x22, // BlockDuration(size=1, value=34) + }; + const int kClusterSize = sizeof(kClusterData); + + int result = parser_->Parse(kClusterData, kClusterSize); + EXPECT_EQ(kClusterSize, result); + ASSERT_TRUE(VerifyBuffers(parser_, kBlockInfo, block_count)); +} + +TEST_F(WebMClusterParserTest, ParseSimpleBlockAndBlockGroupMixture) { + const BlockInfo kBlockInfo[] = { + {kAudioTrackNum, 0, 23, true, NULL, 0}, + {kAudioTrackNum, 23, 23, false, NULL, 0}, + {kVideoTrackNum, 33, 34, true, NULL, 0}, + {kAudioTrackNum, 46, 23, false, NULL, 0}, + {kVideoTrackNum, 67, 33, false, NULL, 0}, + }; + int block_count = arraysize(kBlockInfo); + scoped_ptr cluster(CreateCluster(0, kBlockInfo, block_count)); + + int result = parser_->Parse(cluster->data(), cluster->size()); + EXPECT_EQ(cluster->size(), result); + ASSERT_TRUE(VerifyBuffers(parser_, kBlockInfo, block_count)); +} + +TEST_F(WebMClusterParserTest, IgnoredTracks) { + std::set ignored_tracks; + ignored_tracks.insert(kTextTrackNum); + + parser_.reset(CreateParserWithIgnoredTracks(ignored_tracks)); + + const BlockInfo kInputBlockInfo[] = { + {kAudioTrackNum, 0, 23, true, NULL, 0}, + {kAudioTrackNum, 23, 23, true, NULL, 0}, + {kVideoTrackNum, 33, 34, true, NULL, 0}, + {kTextTrackNum, 33, 99, true, NULL, 0}, + {kAudioTrackNum, 46, 23, true, NULL, 0}, + {kVideoTrackNum, 67, 34, true, NULL, 0}, + }; + int input_block_count = arraysize(kInputBlockInfo); + + const BlockInfo kOutputBlockInfo[] = { + {kAudioTrackNum, 0, 23, true, NULL, 0}, + {kAudioTrackNum, 23, 23, true, NULL, 0}, + {kVideoTrackNum, 33, 34, true, NULL, 0}, + {kAudioTrackNum, 46, 23, true, NULL, 0}, + {kVideoTrackNum, 67, 34, true, NULL, 0}, + }; + int output_block_count = arraysize(kOutputBlockInfo); + + scoped_ptr cluster( + CreateCluster(0, kInputBlockInfo, input_block_count)); + + EXPECT_MEDIA_LOG(WebMSimpleBlockDurationEstimated(23)); + EXPECT_MEDIA_LOG(WebMSimpleBlockDurationEstimated(34)); + int result = parser_->Parse(cluster->data(), cluster->size()); + EXPECT_EQ(cluster->size(), result); + ASSERT_TRUE(VerifyBuffers(parser_, kOutputBlockInfo, output_block_count)); +} + +TEST_F(WebMClusterParserTest, ParseTextTracks) { + TextTracks text_tracks; + + text_tracks.insert(std::make_pair(TextTracks::key_type(kTextTrackNum), + TextTrackConfig(kTextSubtitles, "", "", + ""))); + + parser_.reset(CreateParserWithDefaultDurationsAndOptionalTextTracks( + kNoTimestamp(), kNoTimestamp(), text_tracks)); + + const BlockInfo kInputBlockInfo[] = { + {kAudioTrackNum, 0, 23, true, NULL, 0}, + {kAudioTrackNum, 23, 23, true, NULL, 0}, + {kVideoTrackNum, 33, 34, true, NULL, 0}, + {kTextTrackNum, 33, 42, false, NULL, 0}, + {kAudioTrackNum, 46, 23, true, NULL, 0}, + {kTextTrackNum, 55, 44, false, NULL, 0}, + {kVideoTrackNum, 67, 34, true, NULL, 0}, + }; + int input_block_count = arraysize(kInputBlockInfo); + + scoped_ptr cluster( + CreateCluster(0, kInputBlockInfo, input_block_count)); + + EXPECT_MEDIA_LOG(WebMSimpleBlockDurationEstimated(23)); + EXPECT_MEDIA_LOG(WebMSimpleBlockDurationEstimated(34)); + int result = parser_->Parse(cluster->data(), cluster->size()); + EXPECT_EQ(cluster->size(), result); + ASSERT_TRUE(VerifyBuffers(parser_, kInputBlockInfo, input_block_count)); +} + +TEST_F(WebMClusterParserTest, TextTracksSimpleBlock) { + TextTracks text_tracks; + + text_tracks.insert(std::make_pair(TextTracks::key_type(kTextTrackNum), + TextTrackConfig(kTextSubtitles, "", "", + ""))); + + parser_.reset(CreateParserWithDefaultDurationsAndOptionalTextTracks( + kNoTimestamp(), kNoTimestamp(), text_tracks)); + + const BlockInfo kInputBlockInfo[] = { + { kTextTrackNum, 33, 42, true }, + }; + int input_block_count = arraysize(kInputBlockInfo); + + scoped_ptr cluster( + CreateCluster(0, kInputBlockInfo, input_block_count)); + + int result = parser_->Parse(cluster->data(), cluster->size()); + EXPECT_LT(result, 0); +} + +TEST_F(WebMClusterParserTest, ParseMultipleTextTracks) { + TextTracks text_tracks; + + const int kSubtitleTextTrackNum = kTextTrackNum; + const int kCaptionTextTrackNum = kTextTrackNum + 1; + + text_tracks.insert(std::make_pair(TextTracks::key_type(kSubtitleTextTrackNum), + TextTrackConfig(kTextSubtitles, "", "", + ""))); + + text_tracks.insert(std::make_pair(TextTracks::key_type(kCaptionTextTrackNum), + TextTrackConfig(kTextCaptions, "", "", + ""))); + + parser_.reset(CreateParserWithDefaultDurationsAndOptionalTextTracks( + kNoTimestamp(), kNoTimestamp(), text_tracks)); + + const BlockInfo kInputBlockInfo[] = { + {kAudioTrackNum, 0, 23, true, NULL, 0}, + {kAudioTrackNum, 23, 23, true, NULL, 0}, + {kVideoTrackNum, 33, 34, true, NULL, 0}, + {kSubtitleTextTrackNum, 33, 42, false, NULL, 0}, + {kAudioTrackNum, 46, 23, true, NULL, 0}, + {kCaptionTextTrackNum, 55, 44, false, NULL, 0}, + {kVideoTrackNum, 67, 34, true, NULL, 0}, + {kSubtitleTextTrackNum, 67, 33, false, NULL, 0}, + }; + int input_block_count = arraysize(kInputBlockInfo); + + scoped_ptr cluster( + CreateCluster(0, kInputBlockInfo, input_block_count)); + + EXPECT_MEDIA_LOG(WebMSimpleBlockDurationEstimated(23)); + EXPECT_MEDIA_LOG(WebMSimpleBlockDurationEstimated(34)); + int result = parser_->Parse(cluster->data(), cluster->size()); + EXPECT_EQ(cluster->size(), result); + + const WebMClusterParser::TextBufferQueueMap& text_map = + parser_->GetTextBuffers(); + for (WebMClusterParser::TextBufferQueueMap::const_iterator itr = + text_map.begin(); + itr != text_map.end(); + ++itr) { + const TextTracks::const_iterator find_result = + text_tracks.find(itr->first); + ASSERT_TRUE(find_result != text_tracks.end()); + ASSERT_TRUE(VerifyTextBuffers(parser_, kInputBlockInfo, input_block_count, + itr->first, itr->second)); + } +} + +TEST_F(WebMClusterParserTest, ParseEncryptedBlock) { + scoped_ptr cluster(CreateEncryptedCluster(sizeof(kEncryptedFrame))); + + parser_.reset(CreateParserWithKeyIdsAndAudioCodec( + std::string(), "video_key_id", kUnknownAudioCodec)); + + // The encrypted cluster contains just one block, video. + EXPECT_MEDIA_LOG(WebMSimpleBlockDurationEstimated( + WebMClusterParser::kDefaultVideoBufferDurationInMs)); + + int result = parser_->Parse(cluster->data(), cluster->size()); + EXPECT_EQ(cluster->size(), result); + ASSERT_EQ(1UL, parser_->GetVideoBuffers().size()); + scoped_refptr buffer = parser_->GetVideoBuffers()[0]; + VerifyEncryptedBuffer(buffer); +} + +TEST_F(WebMClusterParserTest, ParseBadEncryptedBlock) { + scoped_ptr cluster( + CreateEncryptedCluster(sizeof(kEncryptedFrame) - 1)); + + parser_.reset(CreateParserWithKeyIdsAndAudioCodec( + std::string(), "video_key_id", kUnknownAudioCodec)); + int result = parser_->Parse(cluster->data(), cluster->size()); + EXPECT_EQ(-1, result); +} + +TEST_F(WebMClusterParserTest, ParseInvalidZeroSizedCluster) { + const uint8_t kBuffer[] = { + 0x1F, 0x43, 0xB6, 0x75, 0x80, // CLUSTER (size = 0) + }; + + EXPECT_EQ(-1, parser_->Parse(kBuffer, sizeof(kBuffer))); +} + +TEST_F(WebMClusterParserTest, ParseInvalidUnknownButActuallyZeroSizedCluster) { + const uint8_t kBuffer[] = { + 0x1F, 0x43, 0xB6, 0x75, 0xFF, // CLUSTER (size = "unknown") + 0x1F, 0x43, 0xB6, 0x75, 0x85, // CLUSTER (size = 5) + }; + + EXPECT_EQ(-1, parser_->Parse(kBuffer, sizeof(kBuffer))); +} + +TEST_F(WebMClusterParserTest, ParseInvalidTextBlockGroupWithoutDuration) { + // Text track frames must have explicitly specified BlockGroup BlockDurations. + TextTracks text_tracks; + + text_tracks.insert(std::make_pair(TextTracks::key_type(kTextTrackNum), + TextTrackConfig(kTextSubtitles, "", "", + ""))); + + parser_.reset(CreateParserWithDefaultDurationsAndOptionalTextTracks( + kNoTimestamp(), kNoTimestamp(), text_tracks)); + + const BlockInfo kBlockInfo[] = { + { kTextTrackNum, 33, -42, false }, + }; + int block_count = arraysize(kBlockInfo); + scoped_ptr cluster(CreateCluster(0, kBlockInfo, block_count)); + int result = parser_->Parse(cluster->data(), cluster->size()); + EXPECT_LT(result, 0); +} + +TEST_F(WebMClusterParserTest, ParseWithDefaultDurationsSimpleBlocks) { + InSequence s; + ResetParserToHaveDefaultDurations(); + + EXPECT_LT(kTestAudioFrameDefaultDurationInMs, 23); + EXPECT_LT(kTestVideoFrameDefaultDurationInMs, 33); + + const BlockInfo kBlockInfo[] = { + {kAudioTrackNum, 0, kTestAudioFrameDefaultDurationInMs, true, NULL, 0}, + {kAudioTrackNum, 23, kTestAudioFrameDefaultDurationInMs, true, NULL, 0}, + {kVideoTrackNum, 33, kTestVideoFrameDefaultDurationInMs, true, NULL, 0}, + {kAudioTrackNum, 46, kTestAudioFrameDefaultDurationInMs, true, NULL, 0}, + {kVideoTrackNum, 67, kTestVideoFrameDefaultDurationInMs, true, NULL, 0}, + {kAudioTrackNum, 69, kTestAudioFrameDefaultDurationInMs, true, NULL, 0}, + {kVideoTrackNum, 100, kTestVideoFrameDefaultDurationInMs, true, NULL, 0}, + }; + + int block_count = arraysize(kBlockInfo); + scoped_ptr cluster(CreateCluster(0, kBlockInfo, block_count)); + + // Send slightly less than the full cluster so all but the last block is + // parsed. Though all the blocks are simple blocks, none should be held aside + // for duration estimation prior to end of cluster detection because all the + // tracks have DefaultDurations. + int result = parser_->Parse(cluster->data(), cluster->size() - 1); + EXPECT_GT(result, 0); + EXPECT_LT(result, cluster->size()); + ASSERT_TRUE(VerifyBuffers(parser_, kBlockInfo, block_count - 1)); + + parser_->Reset(); + + // Now parse a whole cluster to verify that all the blocks will get parsed. + result = parser_->Parse(cluster->data(), cluster->size()); + EXPECT_EQ(cluster->size(), result); + ASSERT_TRUE(VerifyBuffers(parser_, kBlockInfo, block_count)); +} + +TEST_F(WebMClusterParserTest, ParseWithoutAnyDurationsSimpleBlocks) { + InSequence s; + + // Absent DefaultDuration information, SimpleBlock durations are derived from + // inter-buffer track timestamp delta if within the cluster. Duration for the + // last block in a cluster is estimated independently for each track in the + // cluster. For video tracks we use the maximum seen so far. For audio we use + // the the minimum. + // TODO(chcunningham): Move audio over to use the maximum. + + const int kExpectedAudioEstimationInMs = 22; + const int kExpectedVideoEstimationInMs = 34; + const BlockInfo kBlockInfo1[] = { + {kAudioTrackNum, 0, 23, true, NULL, 0}, + {kAudioTrackNum, 23, 22, true, NULL, 0}, + {kVideoTrackNum, 33, 33, true, NULL, 0}, + {kAudioTrackNum, 45, 23, true, NULL, 0}, + {kVideoTrackNum, 66, 34, true, NULL, 0}, + {kAudioTrackNum, 68, kExpectedAudioEstimationInMs, true, NULL, 0}, + {kVideoTrackNum, 100, kExpectedVideoEstimationInMs, true, NULL, 0}, + }; + + int block_count1 = arraysize(kBlockInfo1); + scoped_ptr cluster1(CreateCluster(0, kBlockInfo1, block_count1)); + + // Send slightly less than the first full cluster so all but the last video + // block is parsed. Verify the last fully parsed audio and video buffer are + // both missing from the result (parser should hold them aside for duration + // estimation prior to end of cluster detection in the absence of + // DefaultDurations.) + int result = parser_->Parse(cluster1->data(), cluster1->size() - 1); + EXPECT_GT(result, 0); + EXPECT_LT(result, cluster1->size()); + ASSERT_TRUE(VerifyBuffers(parser_, kBlockInfo1, block_count1 - 3)); + EXPECT_EQ(3UL, parser_->GetAudioBuffers().size()); + EXPECT_EQ(1UL, parser_->GetVideoBuffers().size()); + + parser_->Reset(); + + // Now parse the full first cluster and verify all the blocks are parsed. + EXPECT_MEDIA_LOG( + WebMSimpleBlockDurationEstimated(kExpectedAudioEstimationInMs)); + EXPECT_MEDIA_LOG( + WebMSimpleBlockDurationEstimated(kExpectedVideoEstimationInMs)); + result = parser_->Parse(cluster1->data(), cluster1->size()); + EXPECT_EQ(cluster1->size(), result); + ASSERT_TRUE(VerifyBuffers(parser_, kBlockInfo1, block_count1)); + + // Verify that the estimated frame duration is tracked across clusters for + // each track. + const BlockInfo kBlockInfo2[] = { + // Estimate carries over across clusters + {kAudioTrackNum, 200, kExpectedAudioEstimationInMs, true, NULL, 0}, + // Estimate carries over across clusters + {kVideoTrackNum, 201, kExpectedVideoEstimationInMs, true, NULL, 0}, + }; + + int block_count2 = arraysize(kBlockInfo2); + scoped_ptr cluster2(CreateCluster(0, kBlockInfo2, block_count2)); + EXPECT_MEDIA_LOG( + WebMSimpleBlockDurationEstimated(kExpectedAudioEstimationInMs)); + EXPECT_MEDIA_LOG( + WebMSimpleBlockDurationEstimated(kExpectedVideoEstimationInMs)); + result = parser_->Parse(cluster2->data(), cluster2->size()); + EXPECT_EQ(cluster2->size(), result); + ASSERT_TRUE(VerifyBuffers(parser_, kBlockInfo2, block_count2)); +} + +TEST_F(WebMClusterParserTest, ParseWithoutAnyDurationsBlockGroups) { + InSequence s; + + // Absent DefaultDuration and BlockDuration information, BlockGroup block + // durations are derived from inter-buffer track timestamp delta if within the + // cluster. Duration for the last block in a cluster is estimated + // independently for each track in the cluster. For video tracks we use the + // maximum seen so far. For audio we use the the minimum. + // TODO(chcunningham): Move audio over to use the maximum. + + const int kExpectedAudioEstimationInMs = 22; + const int kExpectedVideoEstimationInMs = 34; + const BlockInfo kBlockInfo1[] = { + {kAudioTrackNum, 0, -23, false, NULL, 0}, + {kAudioTrackNum, 23, -22, false, NULL, 0}, + {kVideoTrackNum, 33, -33, false, NULL, 0}, + {kAudioTrackNum, 45, -23, false, NULL, 0}, + {kVideoTrackNum, 66, -34, false, NULL, 0}, + {kAudioTrackNum, 68, -kExpectedAudioEstimationInMs, false, NULL, 0}, + {kVideoTrackNum, 100, -kExpectedVideoEstimationInMs, false, NULL, 0}, + }; + + int block_count1 = arraysize(kBlockInfo1); + scoped_ptr cluster1(CreateCluster(0, kBlockInfo1, block_count1)); + + // Send slightly less than the first full cluster so all but the last video + // block is parsed. Verify the last fully parsed audio and video buffer are + // both missing from the result (parser should hold them aside for duration + // estimation prior to end of cluster detection in the absence of + // DefaultDurations.) + int result = parser_->Parse(cluster1->data(), cluster1->size() - 1); + EXPECT_GT(result, 0); + EXPECT_LT(result, cluster1->size()); + ASSERT_TRUE(VerifyBuffers(parser_, kBlockInfo1, block_count1 - 3)); + EXPECT_EQ(3UL, parser_->GetAudioBuffers().size()); + EXPECT_EQ(1UL, parser_->GetVideoBuffers().size()); + + parser_->Reset(); + + // Now parse the full first cluster and verify all the blocks are parsed. + EXPECT_MEDIA_LOG( + WebMSimpleBlockDurationEstimated(kExpectedAudioEstimationInMs)); + EXPECT_MEDIA_LOG( + WebMSimpleBlockDurationEstimated(kExpectedVideoEstimationInMs)); + result = parser_->Parse(cluster1->data(), cluster1->size()); + EXPECT_EQ(cluster1->size(), result); + ASSERT_TRUE(VerifyBuffers(parser_, kBlockInfo1, block_count1)); + + // Verify that the estimated frame duration is tracked across clusters for + // each track. + const BlockInfo kBlockInfo2[] = { + {kAudioTrackNum, 200, -kExpectedAudioEstimationInMs, false, NULL, 0}, + {kVideoTrackNum, 201, -kExpectedVideoEstimationInMs, false, NULL, 0}, + }; + + int block_count2 = arraysize(kBlockInfo2); + scoped_ptr cluster2(CreateCluster(0, kBlockInfo2, block_count2)); + EXPECT_MEDIA_LOG( + WebMSimpleBlockDurationEstimated(kExpectedAudioEstimationInMs)); + EXPECT_MEDIA_LOG( + WebMSimpleBlockDurationEstimated(kExpectedVideoEstimationInMs)); + result = parser_->Parse(cluster2->data(), cluster2->size()); + EXPECT_EQ(cluster2->size(), result); + ASSERT_TRUE(VerifyBuffers(parser_, kBlockInfo2, block_count2)); +} + +// TODO(wolenetz): Is parser behavior correct? See http://crbug.com/363433. +TEST_F(WebMClusterParserTest, + ParseWithDefaultDurationsBlockGroupsWithoutDurations) { + InSequence s; + ResetParserToHaveDefaultDurations(); + + EXPECT_LT(kTestAudioFrameDefaultDurationInMs, 23); + EXPECT_LT(kTestVideoFrameDefaultDurationInMs, 33); + + const BlockInfo kBlockInfo[] = { + {kAudioTrackNum, 0, -kTestAudioFrameDefaultDurationInMs, false, NULL, 0}, + {kAudioTrackNum, 23, -kTestAudioFrameDefaultDurationInMs, false, NULL, 0}, + {kVideoTrackNum, 33, -kTestVideoFrameDefaultDurationInMs, false, NULL, 0}, + {kAudioTrackNum, 46, -kTestAudioFrameDefaultDurationInMs, false, NULL, 0}, + {kVideoTrackNum, 67, -kTestVideoFrameDefaultDurationInMs, false, NULL, 0}, + {kAudioTrackNum, 69, -kTestAudioFrameDefaultDurationInMs, false, NULL, 0}, + {kVideoTrackNum, + 100, + -kTestVideoFrameDefaultDurationInMs, + false, + NULL, + 0}, + }; + + int block_count = arraysize(kBlockInfo); + scoped_ptr cluster(CreateCluster(0, kBlockInfo, block_count)); + + // Send slightly less than the full cluster so all but the last block is + // parsed. None should be held aside for duration estimation prior to end of + // cluster detection because all the tracks have DefaultDurations. + int result = parser_->Parse(cluster->data(), cluster->size() - 1); + EXPECT_GT(result, 0); + EXPECT_LT(result, cluster->size()); + ASSERT_TRUE(VerifyBuffers(parser_, kBlockInfo, block_count - 1)); + + parser_->Reset(); + + // Now parse a whole cluster to verify that all the blocks will get parsed. + result = parser_->Parse(cluster->data(), cluster->size()); + EXPECT_EQ(cluster->size(), result); + ASSERT_TRUE(VerifyBuffers(parser_, kBlockInfo, block_count)); +} + +TEST_F(WebMClusterParserTest, + ParseDegenerateClusterYieldsHardcodedEstimatedDurations) { + const BlockInfo kBlockInfo[] = { + { + kAudioTrackNum, + 0, + WebMClusterParser::kDefaultAudioBufferDurationInMs, + true + }, { + kVideoTrackNum, + 0, + WebMClusterParser::kDefaultVideoBufferDurationInMs, + true + }, + }; + + int block_count = arraysize(kBlockInfo); + scoped_ptr cluster(CreateCluster(0, kBlockInfo, block_count)); + EXPECT_MEDIA_LOG(WebMSimpleBlockDurationEstimated( + WebMClusterParser::kDefaultAudioBufferDurationInMs)); + EXPECT_MEDIA_LOG(WebMSimpleBlockDurationEstimated( + WebMClusterParser::kDefaultVideoBufferDurationInMs)); + int result = parser_->Parse(cluster->data(), cluster->size()); + EXPECT_EQ(cluster->size(), result); + ASSERT_TRUE(VerifyBuffers(parser_, kBlockInfo, block_count)); +} + +TEST_F(WebMClusterParserTest, + ParseDegenerateClusterWithDefaultDurationsYieldsDefaultDurations) { + ResetParserToHaveDefaultDurations(); + + const BlockInfo kBlockInfo[] = { + { kAudioTrackNum, 0, kTestAudioFrameDefaultDurationInMs, true }, + { kVideoTrackNum, 0, kTestVideoFrameDefaultDurationInMs, true }, + }; + + int block_count = arraysize(kBlockInfo); + scoped_ptr cluster(CreateCluster(0, kBlockInfo, block_count)); + int result = parser_->Parse(cluster->data(), cluster->size()); + EXPECT_EQ(cluster->size(), result); + ASSERT_TRUE(VerifyBuffers(parser_, kBlockInfo, block_count)); +} + +TEST_F(WebMClusterParserTest, ReadOpusDurationsSimpleBlockAtEndOfCluster) { + int loop_count = 0; + for (const auto* packet_ptr : BuildAllOpusPackets()) { + InSequence s; + + // Get a new parser each iteration to prevent exceeding the media log cap. + parser_.reset(CreateParserWithKeyIdsAndAudioCodec( + std::string(), std::string(), kCodecOpus)); + + const BlockInfo kBlockInfo[] = {{kAudioTrackNum, + 0, + packet_ptr->duration_ms(), + true, // Make it a SimpleBlock. + packet_ptr->data(), + packet_ptr->size()}}; + + int block_count = arraysize(kBlockInfo); + scoped_ptr cluster(CreateCluster(0, kBlockInfo, block_count)); + int duration_ms = packet_ptr->duration_ms(); // Casts from double. + if (duration_ms > 120) { + EXPECT_MEDIA_LOG(OpusPacketDurationTooHigh(duration_ms)); + } + + int result = parser_->Parse(cluster->data(), cluster->size()); + EXPECT_EQ(cluster->size(), result); + ASSERT_TRUE(VerifyBuffers(parser_, kBlockInfo, block_count)); + + // Fail early if any iteration fails to meet the logging expectations. + ASSERT_TRUE(Mock::VerifyAndClearExpectations(media_log_.get())); + + loop_count++; + } + + // Test should minimally cover all the combinations of config and frame count. + ASSERT_GE(loop_count, kNumPossibleOpusConfigs * kMaxOpusPacketFrameCount); +} + +TEST_F(WebMClusterParserTest, PreferOpusDurationsOverBlockDurations) { + int loop_count = 0; + for (const auto* packet_ptr : BuildAllOpusPackets()) { + InSequence s; + + // Get a new parser each iteration to prevent exceeding the media log cap. + parser_.reset(CreateParserWithKeyIdsAndAudioCodec( + std::string(), std::string(), kCodecOpus)); + + // Setting BlockDuration != Opus duration to see which one the parser uses. + int block_duration_ms = packet_ptr->duration_ms() + 10; + if (packet_ptr->duration_ms() > 120) { + EXPECT_MEDIA_LOG(OpusPacketDurationTooHigh(packet_ptr->duration_ms())); + } + + EXPECT_MEDIA_LOG(WebMBlockDurationMismatchesOpusDuration( + block_duration_ms, packet_ptr->duration_ms())); + + BlockInfo block_infos[] = {{kAudioTrackNum, + 0, + block_duration_ms, + false, // Not a SimpleBlock. + packet_ptr->data(), + packet_ptr->size()}}; + + int block_count = arraysize(block_infos); + scoped_ptr cluster(CreateCluster(0, block_infos, block_count)); + int result = parser_->Parse(cluster->data(), cluster->size()); + EXPECT_EQ(cluster->size(), result); + + // BlockInfo duration will be used to verify buffer duration, so changing + // duration to be that of the Opus packet to verify it was preferred. + block_infos[0].duration = packet_ptr->duration_ms(); + + ASSERT_TRUE(VerifyBuffers(parser_, block_infos, block_count)); + + // Fail early if any iteration fails to meet the logging expectations. + ASSERT_TRUE(Mock::VerifyAndClearExpectations(media_log_.get())); + + loop_count++; + } + + // Test should minimally cover all the combinations of config and frame count. + ASSERT_GE(loop_count, kNumPossibleOpusConfigs * kMaxOpusPacketFrameCount); +} + +// Tests that BlockDuration is used to set duration on buffer rather than +// encoded duration in Opus packet (or hard coded duration estimates). Encoded +// Opus duration is usually preferred but cannot be known when encrypted. +TEST_F(WebMClusterParserTest, DontReadEncodedDurationWhenEncrypted) { + // Non-empty dummy value signals encryption is active for audio. + std::string audio_encryption_id("audio_key_id"); + + // Reset parser to expect Opus codec audio and use audio encryption key id. + parser_.reset(CreateParserWithKeyIdsAndAudioCodec(audio_encryption_id, + std::string(), kCodecOpus)); + + // Single Block with BlockDuration and encrypted data. + const BlockInfo kBlockInfo[] = {{kAudioTrackNum, + 0, + kTestAudioFrameDefaultDurationInMs, + false, // Not a SimpleBlock + kEncryptedFrame, // Encrypted frame data + arraysize(kEncryptedFrame)}}; + + int block_count = arraysize(kBlockInfo); + scoped_ptr cluster(CreateCluster(0, kBlockInfo, block_count)); + int result = parser_->Parse(cluster->data(), cluster->size()); + EXPECT_EQ(cluster->size(), result); + + // Will verify that duration of buffer matches that of BlockDuration. + ASSERT_TRUE(VerifyBuffers(parser_, kBlockInfo, block_count)); +} + +} // namespace media diff --git a/packager/media/formats/webm/webm_constants.cc b/packager/media/formats/webm/webm_constants.cc new file mode 100644 index 0000000000..d6c5536fa3 --- /dev/null +++ b/packager/media/formats/webm/webm_constants.cc @@ -0,0 +1,14 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "media/formats/webm/webm_constants.h" + +namespace media { + +const char kWebMCodecSubtitles[] = "D_WEBVTT/SUBTITLES"; +const char kWebMCodecCaptions[] = "D_WEBVTT/CAPTIONS"; +const char kWebMCodecDescriptions[] = "D_WEBVTT/DESCRIPTIONS"; +const char kWebMCodecMetadata[] = "D_WEBVTT/METADATA"; + +} // namespace media diff --git a/packager/media/formats/webm/webm_constants.h b/packager/media/formats/webm/webm_constants.h new file mode 100644 index 0000000000..8a0b8a7185 --- /dev/null +++ b/packager/media/formats/webm/webm_constants.h @@ -0,0 +1,229 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef MEDIA_FORMATS_WEBM_WEBM_CONSTANTS_H_ +#define MEDIA_FORMATS_WEBM_WEBM_CONSTANTS_H_ + +#include "base/basictypes.h" +#include "media/base/media_export.h" + +namespace media { + +// WebM element IDs. +// This is a subset of the IDs in the Matroska spec. +// http://www.matroska.org/technical/specs/index.html +const int kWebMIdAESSettingsCipherMode = 0x47E8; +const int kWebMIdAlphaMode = 0x53C0; +const int kWebMIdAspectRatioType = 0x54B3; +const int kWebMIdAttachedFile = 0x61A7; +const int kWebMIdAttachmentLink = 0x7446; +const int kWebMIdAttachments = 0x1941A469; +const int kWebMIdAudio = 0xE1; +const int kWebMIdBitDepth = 0x6264; +const int kWebMIdBlock = 0xA1; +const int kWebMIdBlockAddID = 0xEE; +const int kWebMIdBlockAdditions = 0x75A1; +const int kWebMIdBlockAdditional = 0xA5; +const int kWebMIdBlockDuration = 0x9B; +const int kWebMIdBlockGroup = 0xA0; +const int kWebMIdBlockMore = 0xA6; +const int kWebMIdChannels = 0x9F; +const int kWebMIdChapCountry = 0x437E; +const int kWebMIdChapLanguage = 0x437C; +const int kWebMIdChapProcess = 0x6944; +const int kWebMIdChapProcessCodecID = 0x6955; +const int kWebMIdChapProcessCommand = 0x6911; +const int kWebMIdChapProcessData = 0x6933; +const int kWebMIdChapProcessPrivate = 0x450D; +const int kWebMIdChapProcessTime = 0x6922; +const int kWebMIdChapString = 0x85; +const int kWebMIdChapterAtom = 0xB6; +const int kWebMIdChapterDisplay = 0x80; +const int kWebMIdChapterFlagEnabled = 0x4598; +const int kWebMIdChapterFlagHidden = 0x98; +const int kWebMIdChapterPhysicalEquiv = 0x63C3; +const int kWebMIdChapters = 0x1043A770; +const int kWebMIdChapterSegmentEditionUID = 0x6EBC; +const int kWebMIdChapterSegmentUID = 0x6E67; +const int kWebMIdChapterTimeEnd = 0x92; +const int kWebMIdChapterTimeStart = 0x91; +const int kWebMIdChapterTrack = 0x8F; +const int kWebMIdChapterTrackNumber = 0x89; +const int kWebMIdChapterTranslate = 0x6924; +const int kWebMIdChapterTranslateCodec = 0x69BF; +const int kWebMIdChapterTranslateEditionUID = 0x69FC; +const int kWebMIdChapterTranslateID = 0x69A5; +const int kWebMIdChapterUID = 0x73C4; +const int kWebMIdCluster = 0x1F43B675; +const int kWebMIdCodecDecodeAll = 0xAA; +const int kWebMIdCodecDelay = 0x56AA; +const int kWebMIdCodecID = 0x86; +const int kWebMIdCodecName = 0x258688; +const int kWebMIdCodecPrivate = 0x63A2; +const int kWebMIdCodecState = 0xA4; +const int kWebMIdColorSpace = 0x2EB524; +const int kWebMIdContentCompAlgo = 0x4254; +const int kWebMIdContentCompression = 0x5034; +const int kWebMIdContentCompSettings = 0x4255; +const int kWebMIdContentEncAESSettings = 0x47E7; +const int kWebMIdContentEncAlgo = 0x47E1; +const int kWebMIdContentEncKeyID = 0x47E2; +const int kWebMIdContentEncoding = 0x6240; +const int kWebMIdContentEncodingOrder = 0x5031; +const int kWebMIdContentEncodings = 0x6D80; +const int kWebMIdContentEncodingScope = 0x5032; +const int kWebMIdContentEncodingType = 0x5033; +const int kWebMIdContentEncryption = 0x5035; +const int kWebMIdContentSigAlgo = 0x47E5; +const int kWebMIdContentSigHashAlgo = 0x47E6; +const int kWebMIdContentSigKeyID = 0x47E4; +const int kWebMIdContentSignature = 0x47E3; +const int kWebMIdCRC32 = 0xBF; +const int kWebMIdCueBlockNumber = 0x5378; +const int kWebMIdCueClusterPosition = 0xF1; +const int kWebMIdCueCodecState = 0xEA; +const int kWebMIdCuePoint = 0xBB; +const int kWebMIdCueReference = 0xDB; +const int kWebMIdCueRefTime = 0x96; +const int kWebMIdCues = 0x1C53BB6B; +const int kWebMIdCueTime = 0xB3; +const int kWebMIdCueTrack = 0xF7; +const int kWebMIdCueTrackPositions = 0xB7; +const int kWebMIdDateUTC = 0x4461; +const int kWebMIdDefaultDuration = 0x23E383; +const int kWebMIdDiscardPadding = 0x75A2; +const int kWebMIdDisplayHeight = 0x54BA; +const int kWebMIdDisplayUnit = 0x54B2; +const int kWebMIdDisplayWidth = 0x54B0; +const int kWebMIdDocType = 0x4282; +const int kWebMIdDocTypeReadVersion = 0x4285; +const int kWebMIdDocTypeVersion = 0x4287; +const int kWebMIdDuration = 0x4489; +const int kWebMIdEBMLHeader = 0x1A45DFA3; +const int kWebMIdEBMLMaxIDLength = 0x42F2; +const int kWebMIdEBMLMaxSizeLength = 0x42F3; +const int kWebMIdEBMLReadVersion = 0x42F7; +const int kWebMIdEBMLVersion = 0x4286; +const int kWebMIdEditionEntry = 0x45B9; +const int kWebMIdEditionFlagDefault = 0x45DB; +const int kWebMIdEditionFlagHidden = 0x45BD; +const int kWebMIdEditionFlagOrdered = 0x45DD; +const int kWebMIdEditionUID = 0x45BC; +const int kWebMIdFileData = 0x465C; +const int kWebMIdFileDescription = 0x467E; +const int kWebMIdFileMimeType = 0x4660; +const int kWebMIdFileName = 0x466E; +const int kWebMIdFileUID = 0x46AE; +const int kWebMIdFlagDefault = 0x88; +const int kWebMIdFlagEnabled = 0xB9; +const int kWebMIdFlagForced = 0x55AA; +const int kWebMIdFlagInterlaced = 0x9A; +const int kWebMIdFlagLacing = 0x9C; +const int kWebMIdFrameRate = 0x2383E3; +const int kWebMIdInfo = 0x1549A966; +const int kWebMIdJoinBlocks = 0xE9; +const int kWebMIdLaceNumber = 0xCC; +const int kWebMIdLanguage = 0x22B59C; +const int kWebMIdMaxBlockAdditionId = 0x55EE; +const int kWebMIdMaxCache = 0x6DF8; +const int kWebMIdMinCache = 0x6DE7; +const int kWebMIdMuxingApp = 0x4D80; +const int kWebMIdName = 0x536E; +const int kWebMIdNextFilename = 0x3E83BB; +const int kWebMIdNextUID = 0x3EB923; +const int kWebMIdOutputSamplingFrequency = 0x78B5; +const int kWebMIdPixelCropBottom = 0x54AA; +const int kWebMIdPixelCropLeft = 0x54CC; +const int kWebMIdPixelCropRight = 0x54DD; +const int kWebMIdPixelCropTop = 0x54BB; +const int kWebMIdPixelHeight = 0xBA; +const int kWebMIdPixelWidth = 0xB0; +const int kWebMIdPosition = 0xA7; +const int kWebMIdPrevFilename = 0x3C83AB; +const int kWebMIdPrevSize = 0xAB; +const int kWebMIdPrevUID = 0x3CB923; +const int kWebMIdReferenceBlock = 0xFB; +const int kWebMIdReferencePriority = 0xFA; +const int kWebMIdSamplingFrequency = 0xB5; +const int kWebMIdSeek = 0x4DBB; +const int kWebMIdSeekHead = 0x114D9B74; +const int kWebMIdSeekID = 0x53AB; +const int kWebMIdSeekPosition = 0x53AC; +const int kWebMIdSeekPreRoll = 0x56BB; +const int kWebMIdSegment = 0x18538067; +const int kWebMIdSegmentFamily = 0x4444; +const int kWebMIdSegmentFilename = 0x7384; +const int kWebMIdSegmentUID = 0x73A4; +const int kWebMIdSilentTrackNumber = 0x58D7; +const int kWebMIdSilentTracks = 0x5854; +const int kWebMIdSimpleBlock = 0xA3; +const int kWebMIdSimpleTag = 0x67C8; +const int kWebMIdSlices = 0x8E; +const int kWebMIdStereoMode = 0x53B8; +const int kWebMIdTag = 0x7373; +const int kWebMIdTagAttachmentUID = 0x63C6; +const int kWebMIdTagBinary = 0x4485; +const int kWebMIdTagChapterUID = 0x63C4; +const int kWebMIdTagDefault = 0x4484; +const int kWebMIdTagEditionUID = 0x63C9; +const int kWebMIdTagLanguage = 0x447A; +const int kWebMIdTagName = 0x45A3; +const int kWebMIdTags = 0x1254C367; +const int kWebMIdTagString = 0x4487; +const int kWebMIdTagTrackUID = 0x63C5; +const int kWebMIdTargets = 0x63C0; +const int kWebMIdTargetType = 0x63CA; +const int kWebMIdTargetTypeValue = 0x68CA; +const int kWebMIdTimecode = 0xE7; +const int kWebMIdTimecodeScale = 0x2AD7B1; +const int kWebMIdTimeSlice = 0xE8; +const int kWebMIdTitle = 0x7BA9; +const int kWebMIdTrackCombinePlanes = 0xE3; +const int kWebMIdTrackEntry = 0xAE; +const int kWebMIdTrackJoinUID = 0xED; +const int kWebMIdTrackNumber = 0xD7; +const int kWebMIdTrackOperation = 0xE2; +const int kWebMIdTrackOverlay = 0x6FAB; +const int kWebMIdTrackPlane = 0xE4; +const int kWebMIdTrackPlaneType = 0xE6; +const int kWebMIdTrackPlaneUID = 0xE5; +const int kWebMIdTracks = 0x1654AE6B; +const int kWebMIdTrackTimecodeScale = 0x23314F; +const int kWebMIdTrackTranslate = 0x6624; +const int kWebMIdTrackTranslateCodec = 0x66BF; +const int kWebMIdTrackTranslateEditionUID = 0x66FC; +const int kWebMIdTrackTranslateTrackID = 0x66A5; +const int kWebMIdTrackType = 0x83; +const int kWebMIdTrackUID = 0x73C5; +const int kWebMIdVideo = 0xE0; +const int kWebMIdVoid = 0xEC; +const int kWebMIdWritingApp = 0x5741; + +const int64 kWebMReservedId = 0x1FFFFFFF; +const int64 kWebMUnknownSize = 0x00FFFFFFFFFFFFFFLL; + +const uint8 kWebMFlagKeyframe = 0x80; + +// Current encrypted WebM request for comments specification is here +// http://wiki.webmproject.org/encryption/webm-encryption-rfc +const uint8 kWebMFlagEncryptedFrame = 0x1; +const int kWebMIvSize = 8; +const int kWebMSignalByteSize = 1; + +// Current specification for WebVTT embedded in WebM +// http://wiki.webmproject.org/webm-metadata/temporal-metadata/webvtt-in-webm + +const int kWebMTrackTypeVideo = 1; +const int kWebMTrackTypeAudio = 2; +const int kWebMTrackTypeSubtitlesOrCaptions = 0x11; +const int kWebMTrackTypeDescriptionsOrMetadata = 0x21; + +MEDIA_EXPORT extern const char kWebMCodecSubtitles[]; +MEDIA_EXPORT extern const char kWebMCodecCaptions[]; +MEDIA_EXPORT extern const char kWebMCodecDescriptions[]; +MEDIA_EXPORT extern const char kWebMCodecMetadata[]; + +} // namespace media + +#endif // MEDIA_FORMATS_WEBM_WEBM_CONSTANTS_H_ diff --git a/packager/media/formats/webm/webm_content_encodings.cc b/packager/media/formats/webm/webm_content_encodings.cc new file mode 100644 index 0000000000..157c6ac431 --- /dev/null +++ b/packager/media/formats/webm/webm_content_encodings.cc @@ -0,0 +1,28 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/logging.h" +#include "media/formats/webm/webm_content_encodings.h" + +namespace media { + +ContentEncoding::ContentEncoding() + : order_(kOrderInvalid), + scope_(kScopeInvalid), + type_(kTypeInvalid), + encryption_algo_(kEncAlgoInvalid), + cipher_mode_(kCipherModeInvalid) { +} + +ContentEncoding::~ContentEncoding() {} + +void ContentEncoding::SetEncryptionKeyId(const uint8* encryption_key_id, + int size) { + DCHECK(encryption_key_id); + DCHECK_GT(size, 0); + encryption_key_id_.assign(reinterpret_cast(encryption_key_id), + size); +} + +} // namespace media diff --git a/packager/media/formats/webm/webm_content_encodings.h b/packager/media/formats/webm/webm_content_encodings.h new file mode 100644 index 0000000000..5890ecf04f --- /dev/null +++ b/packager/media/formats/webm/webm_content_encodings.h @@ -0,0 +1,88 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef MEDIA_FORMATS_WEBM_WEBM_CONTENT_ENCODINGS_H_ +#define MEDIA_FORMATS_WEBM_WEBM_CONTENT_ENCODINGS_H_ + +#include + +#include "base/basictypes.h" +#include "base/memory/scoped_ptr.h" +#include "media/base/media_export.h" + +namespace media { + +class MEDIA_EXPORT ContentEncoding { + public: + // The following enum definitions are based on the ContentEncoding element + // specified in the Matroska spec. + + static const int kOrderInvalid = -1; + + enum Scope { + kScopeInvalid = 0, + kScopeAllFrameContents = 1, + kScopeTrackPrivateData = 2, + kScopeNextContentEncodingData = 4, + kScopeMax = 7, + }; + + enum Type { + kTypeInvalid = -1, + kTypeCompression = 0, + kTypeEncryption = 1, + }; + + enum EncryptionAlgo { + kEncAlgoInvalid = -1, + kEncAlgoNotEncrypted = 0, + kEncAlgoDes = 1, + kEncAlgo3des = 2, + kEncAlgoTwofish = 3, + kEncAlgoBlowfish = 4, + kEncAlgoAes = 5, + }; + + enum CipherMode { + kCipherModeInvalid = 0, + kCipherModeCtr = 1, + }; + + ContentEncoding(); + ~ContentEncoding(); + + int64 order() const { return order_; } + void set_order(int64 order) { order_ = order; } + + Scope scope() const { return scope_; } + void set_scope(Scope scope) { scope_ = scope; } + + Type type() const { return type_; } + void set_type(Type type) { type_ = type; } + + EncryptionAlgo encryption_algo() const { return encryption_algo_; } + void set_encryption_algo(EncryptionAlgo encryption_algo) { + encryption_algo_ = encryption_algo; + } + + const std::string& encryption_key_id() const { return encryption_key_id_; } + void SetEncryptionKeyId(const uint8* encryption_key_id, int size); + + CipherMode cipher_mode() const { return cipher_mode_; } + void set_cipher_mode(CipherMode mode) { cipher_mode_ = mode; } + + private: + int64 order_; + Scope scope_; + Type type_; + EncryptionAlgo encryption_algo_; + std::string encryption_key_id_; + CipherMode cipher_mode_; + + DISALLOW_COPY_AND_ASSIGN(ContentEncoding); +}; + +} // namespace media + +#endif // MEDIA_FORMATS_WEBM_WEBM_CONTENT_ENCODINGS_H_ diff --git a/packager/media/formats/webm/webm_content_encodings_client.cc b/packager/media/formats/webm/webm_content_encodings_client.cc new file mode 100644 index 0000000000..a9783ddf99 --- /dev/null +++ b/packager/media/formats/webm/webm_content_encodings_client.cc @@ -0,0 +1,274 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "media/formats/webm/webm_content_encodings_client.h" + +#include "base/logging.h" +#include "base/stl_util.h" +#include "media/formats/webm/webm_constants.h" + +namespace media { + +WebMContentEncodingsClient::WebMContentEncodingsClient( + const scoped_refptr& media_log) + : media_log_(media_log), + content_encryption_encountered_(false), + content_encodings_ready_(false) { +} + +WebMContentEncodingsClient::~WebMContentEncodingsClient() { + STLDeleteElements(&content_encodings_); +} + +const ContentEncodings& WebMContentEncodingsClient::content_encodings() const { + DCHECK(content_encodings_ready_); + return content_encodings_; +} + +WebMParserClient* WebMContentEncodingsClient::OnListStart(int id) { + if (id == kWebMIdContentEncodings) { + DCHECK(!cur_content_encoding_.get()); + DCHECK(!content_encryption_encountered_); + STLDeleteElements(&content_encodings_); + content_encodings_ready_ = false; + return this; + } + + if (id == kWebMIdContentEncoding) { + DCHECK(!cur_content_encoding_.get()); + DCHECK(!content_encryption_encountered_); + cur_content_encoding_.reset(new ContentEncoding()); + return this; + } + + if (id == kWebMIdContentEncryption) { + DCHECK(cur_content_encoding_.get()); + if (content_encryption_encountered_) { + MEDIA_LOG(ERROR, media_log_) << "Unexpected multiple ContentEncryption."; + return NULL; + } + content_encryption_encountered_ = true; + return this; + } + + if (id == kWebMIdContentEncAESSettings) { + DCHECK(cur_content_encoding_.get()); + return this; + } + + // This should not happen if WebMListParser is working properly. + DCHECK(false); + return NULL; +} + +// Mandatory occurrence restriction is checked in this function. Multiple +// occurrence restriction is checked in OnUInt and OnBinary. +bool WebMContentEncodingsClient::OnListEnd(int id) { + if (id == kWebMIdContentEncodings) { + // ContentEncoding element is mandatory. Check this! + if (content_encodings_.empty()) { + MEDIA_LOG(ERROR, media_log_) << "Missing ContentEncoding."; + return false; + } + content_encodings_ready_ = true; + return true; + } + + if (id == kWebMIdContentEncoding) { + DCHECK(cur_content_encoding_.get()); + + // + // Specify default values to missing mandatory elements. + // + + if (cur_content_encoding_->order() == ContentEncoding::kOrderInvalid) { + // Default value of encoding order is 0, which should only be used on the + // first ContentEncoding. + if (!content_encodings_.empty()) { + MEDIA_LOG(ERROR, media_log_) << "Missing ContentEncodingOrder."; + return false; + } + cur_content_encoding_->set_order(0); + } + + if (cur_content_encoding_->scope() == ContentEncoding::kScopeInvalid) + cur_content_encoding_->set_scope(ContentEncoding::kScopeAllFrameContents); + + if (cur_content_encoding_->type() == ContentEncoding::kTypeInvalid) + cur_content_encoding_->set_type(ContentEncoding::kTypeCompression); + + // Check for elements valid in spec but not supported for now. + if (cur_content_encoding_->type() == ContentEncoding::kTypeCompression) { + MEDIA_LOG(ERROR, media_log_) << "ContentCompression not supported."; + return false; + } + + // Enforce mandatory elements without default values. + DCHECK(cur_content_encoding_->type() == ContentEncoding::kTypeEncryption); + if (!content_encryption_encountered_) { + MEDIA_LOG(ERROR, media_log_) << "ContentEncodingType is encryption but" + << " ContentEncryption is missing."; + return false; + } + + content_encodings_.push_back(cur_content_encoding_.release()); + content_encryption_encountered_ = false; + return true; + } + + if (id == kWebMIdContentEncryption) { + DCHECK(cur_content_encoding_.get()); + // Specify default value for elements that are not present. + if (cur_content_encoding_->encryption_algo() == + ContentEncoding::kEncAlgoInvalid) { + cur_content_encoding_->set_encryption_algo( + ContentEncoding::kEncAlgoNotEncrypted); + } + return true; + } + + if (id == kWebMIdContentEncAESSettings) { + if (cur_content_encoding_->cipher_mode() == + ContentEncoding::kCipherModeInvalid) + cur_content_encoding_->set_cipher_mode(ContentEncoding::kCipherModeCtr); + return true; + } + + // This should not happen if WebMListParser is working properly. + DCHECK(false); + return false; +} + +// Multiple occurrence restriction and range are checked in this function. +// Mandatory occurrence restriction is checked in OnListEnd. +bool WebMContentEncodingsClient::OnUInt(int id, int64 val) { + DCHECK(cur_content_encoding_.get()); + + if (id == kWebMIdContentEncodingOrder) { + if (cur_content_encoding_->order() != ContentEncoding::kOrderInvalid) { + MEDIA_LOG(ERROR, media_log_) + << "Unexpected multiple ContentEncodingOrder."; + return false; + } + + if (val != static_cast(content_encodings_.size())) { + // According to the spec, encoding order starts with 0 and counts upwards. + MEDIA_LOG(ERROR, media_log_) << "Unexpected ContentEncodingOrder."; + return false; + } + + cur_content_encoding_->set_order(val); + return true; + } + + if (id == kWebMIdContentEncodingScope) { + if (cur_content_encoding_->scope() != ContentEncoding::kScopeInvalid) { + MEDIA_LOG(ERROR, media_log_) + << "Unexpected multiple ContentEncodingScope."; + return false; + } + + if (val == ContentEncoding::kScopeInvalid || + val > ContentEncoding::kScopeMax) { + MEDIA_LOG(ERROR, media_log_) << "Unexpected ContentEncodingScope."; + return false; + } + + if (val & ContentEncoding::kScopeNextContentEncodingData) { + MEDIA_LOG(ERROR, media_log_) << "Encoded next ContentEncoding is not " + "supported."; + return false; + } + + cur_content_encoding_->set_scope(static_cast(val)); + return true; + } + + if (id == kWebMIdContentEncodingType) { + if (cur_content_encoding_->type() != ContentEncoding::kTypeInvalid) { + MEDIA_LOG(ERROR, media_log_) + << "Unexpected multiple ContentEncodingType."; + return false; + } + + if (val == ContentEncoding::kTypeCompression) { + MEDIA_LOG(ERROR, media_log_) << "ContentCompression not supported."; + return false; + } + + if (val != ContentEncoding::kTypeEncryption) { + MEDIA_LOG(ERROR, media_log_) << "Unexpected ContentEncodingType " << val + << "."; + return false; + } + + cur_content_encoding_->set_type(static_cast(val)); + return true; + } + + if (id == kWebMIdContentEncAlgo) { + if (cur_content_encoding_->encryption_algo() != + ContentEncoding::kEncAlgoInvalid) { + MEDIA_LOG(ERROR, media_log_) << "Unexpected multiple ContentEncAlgo."; + return false; + } + + if (val < ContentEncoding::kEncAlgoNotEncrypted || + val > ContentEncoding::kEncAlgoAes) { + MEDIA_LOG(ERROR, media_log_) << "Unexpected ContentEncAlgo " << val + << "."; + return false; + } + + cur_content_encoding_->set_encryption_algo( + static_cast(val)); + return true; + } + + if (id == kWebMIdAESSettingsCipherMode) { + if (cur_content_encoding_->cipher_mode() != + ContentEncoding::kCipherModeInvalid) { + MEDIA_LOG(ERROR, media_log_) + << "Unexpected multiple AESSettingsCipherMode."; + return false; + } + + if (val != ContentEncoding::kCipherModeCtr) { + MEDIA_LOG(ERROR, media_log_) << "Unexpected AESSettingsCipherMode " << val + << "."; + return false; + } + + cur_content_encoding_->set_cipher_mode( + static_cast(val)); + return true; + } + + // This should not happen if WebMListParser is working properly. + DCHECK(false); + return false; +} + +// Multiple occurrence restriction is checked in this function. Mandatory +// restriction is checked in OnListEnd. +bool WebMContentEncodingsClient::OnBinary(int id, const uint8* data, int size) { + DCHECK(cur_content_encoding_.get()); + DCHECK(data); + DCHECK_GT(size, 0); + + if (id == kWebMIdContentEncKeyID) { + if (!cur_content_encoding_->encryption_key_id().empty()) { + MEDIA_LOG(ERROR, media_log_) << "Unexpected multiple ContentEncKeyID"; + return false; + } + cur_content_encoding_->SetEncryptionKeyId(data, size); + return true; + } + + // This should not happen if WebMListParser is working properly. + DCHECK(false); + return false; +} + +} // namespace media diff --git a/packager/media/formats/webm/webm_content_encodings_client.h b/packager/media/formats/webm/webm_content_encodings_client.h new file mode 100644 index 0000000000..85f7bf329a --- /dev/null +++ b/packager/media/formats/webm/webm_content_encodings_client.h @@ -0,0 +1,50 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef MEDIA_FORMATS_WEBM_WEBM_CONTENT_ENCODINGS_CLIENT_H_ +#define MEDIA_FORMATS_WEBM_WEBM_CONTENT_ENCODINGS_CLIENT_H_ + +#include + +#include "base/callback.h" +#include "base/compiler_specific.h" +#include "base/memory/scoped_ptr.h" +#include "media/base/media_export.h" +#include "media/base/media_log.h" +#include "media/formats/webm/webm_content_encodings.h" +#include "media/formats/webm/webm_parser.h" + +namespace media { + +typedef std::vector ContentEncodings; + +// Parser for WebM ContentEncodings element. +class MEDIA_EXPORT WebMContentEncodingsClient : public WebMParserClient { + public: + explicit WebMContentEncodingsClient(const scoped_refptr& media_log); + ~WebMContentEncodingsClient() override; + + const ContentEncodings& content_encodings() const; + + // WebMParserClient methods + WebMParserClient* OnListStart(int id) override; + bool OnListEnd(int id) override; + bool OnUInt(int id, int64 val) override; + bool OnBinary(int id, const uint8* data, int size) override; + + private: + scoped_refptr media_log_; + scoped_ptr cur_content_encoding_; + bool content_encryption_encountered_; + ContentEncodings content_encodings_; + + // |content_encodings_| is ready. For debugging purpose. + bool content_encodings_ready_; + + DISALLOW_COPY_AND_ASSIGN(WebMContentEncodingsClient); +}; + +} // namespace media + +#endif // MEDIA_FORMATS_WEBM_WEBM_CONTENT_ENCODINGS_CLIENT_H_ diff --git a/packager/media/formats/webm/webm_content_encodings_client_unittest.cc b/packager/media/formats/webm/webm_content_encodings_client_unittest.cc new file mode 100644 index 0000000000..d31ad278b9 --- /dev/null +++ b/packager/media/formats/webm/webm_content_encodings_client_unittest.cc @@ -0,0 +1,282 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "media/formats/webm/webm_content_encodings_client.h" + +#include + +#include "base/bind.h" +#include "base/strings/string_number_conversions.h" +#include "media/base/mock_media_log.h" +#include "media/formats/webm/webm_constants.h" +#include "media/formats/webm/webm_parser.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using ::testing::StrictMock; + +namespace media { + +// Matchers for verifying common media log entry strings. +MATCHER(MissingContentEncoding, "") { + return CONTAINS_STRING(arg, "Missing ContentEncoding."); +} + +MATCHER(UnexpectedContentEncodingOrder, "") { + return CONTAINS_STRING(arg, "Unexpected ContentEncodingOrder."); +} + +MATCHER(UnexpectedContentEncodingScope, "") { + return CONTAINS_STRING(arg, "Unexpected ContentEncodingScope."); +} + +MATCHER(ContentCompressionNotSupported, "") { + return CONTAINS_STRING(arg, "ContentCompression not supported."); +} + +MATCHER(MissingContentEncryption, "") { + return CONTAINS_STRING( + arg, + "ContentEncodingType is encryption but ContentEncryption is missing."); +} + +MATCHER_P(UnexpectedContentEncAlgo, algo, "") { + return CONTAINS_STRING( + arg, "Unexpected ContentEncAlgo " + base::IntToString(algo) + "."); +} + +class WebMContentEncodingsClientTest : public testing::Test { + public: + WebMContentEncodingsClientTest() + : media_log_(new StrictMock()), + client_(media_log_), + parser_(kWebMIdContentEncodings, &client_) {} + + void ParseAndExpectToFail(const uint8* buf, int size) { + int result = parser_.Parse(buf, size); + EXPECT_EQ(-1, result); + } + + protected: + scoped_refptr> media_log_; + WebMContentEncodingsClient client_; + WebMListParser parser_; +}; + +TEST_F(WebMContentEncodingsClientTest, EmptyContentEncodings) { + const uint8 kContentEncodings[] = { + 0x6D, 0x80, 0x80, // ContentEncodings (size = 0) + }; + int size = sizeof(kContentEncodings); + EXPECT_MEDIA_LOG(MissingContentEncoding()); + ParseAndExpectToFail(kContentEncodings, size); +} + +TEST_F(WebMContentEncodingsClientTest, EmptyContentEncoding) { + const uint8 kContentEncodings[] = { + 0x6D, 0x80, 0x83, // ContentEncodings (size = 3) + 0x63, 0x40, 0x80, // ContentEncoding (size = 0) + }; + int size = sizeof(kContentEncodings); + ParseAndExpectToFail(kContentEncodings, size); +} + +TEST_F(WebMContentEncodingsClientTest, SingleContentEncoding) { + const uint8 kContentEncodings[] = { + 0x6D, 0x80, 0xA1, // ContentEncodings (size = 33) + 0x62, 0x40, 0x9e, // ContentEncoding (size = 30) + 0x50, 0x31, 0x81, 0x00, // ContentEncodingOrder (size = 1) + 0x50, 0x32, 0x81, 0x01, // ContentEncodingScope (size = 1) + 0x50, 0x33, 0x81, 0x01, // ContentEncodingType (size = 1) + 0x50, 0x35, 0x8F, // ContentEncryption (size = 15) + 0x47, 0xE1, 0x81, 0x05, // ContentEncAlgo (size = 1) + 0x47, 0xE2, 0x88, // ContentEncKeyID (size = 8) + 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, + }; + int size = sizeof(kContentEncodings); + + int result = parser_.Parse(kContentEncodings, size); + ASSERT_EQ(size, result); + const ContentEncodings& content_encodings = client_.content_encodings(); + + ASSERT_EQ(1u, content_encodings.size()); + ASSERT_TRUE(content_encodings[0]); + EXPECT_EQ(0, content_encodings[0]->order()); + EXPECT_EQ(ContentEncoding::kScopeAllFrameContents, + content_encodings[0]->scope()); + EXPECT_EQ(ContentEncoding::kTypeEncryption, content_encodings[0]->type()); + EXPECT_EQ(ContentEncoding::kEncAlgoAes, + content_encodings[0]->encryption_algo()); + EXPECT_EQ(8u, content_encodings[0]->encryption_key_id().size()); +} + +TEST_F(WebMContentEncodingsClientTest, MultipleContentEncoding) { + const uint8 kContentEncodings[] = { + 0x6D, 0x80, 0xC2, // ContentEncodings (size = 66) + 0x62, 0x40, 0x9e, // ContentEncoding (size = 30) + 0x50, 0x31, 0x81, 0x00, // ContentEncodingOrder (size = 1) + 0x50, 0x32, 0x81, 0x03, // ContentEncodingScope (size = 1) + 0x50, 0x33, 0x81, 0x01, // ContentEncodingType (size = 1) + 0x50, 0x35, 0x8F, // ContentEncryption (size = 15) + 0x47, 0xE1, 0x81, 0x05, // ContentEncAlgo (size = 1) + 0x47, 0xE2, 0x88, // ContentEncKeyID (size = 8) + 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, + 0x62, 0x40, 0x9e, // ContentEncoding (size = 30) + 0x50, 0x31, 0x81, 0x01, // ContentEncodingOrder (size = 1) + 0x50, 0x32, 0x81, 0x03, // ContentEncodingScope (size = 1) + 0x50, 0x33, 0x81, 0x01, // ContentEncodingType (size = 1) + 0x50, 0x35, 0x8F, // ContentEncryption (size = 15) + 0x47, 0xE1, 0x81, 0x01, // ContentEncAlgo (size = 1) + 0x47, 0xE2, 0x88, // ContentEncKeyID (size = 8) + 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, + }; + int size = sizeof(kContentEncodings); + + int result = parser_.Parse(kContentEncodings, size); + ASSERT_EQ(size, result); + const ContentEncodings& content_encodings = client_.content_encodings(); + ASSERT_EQ(2u, content_encodings.size()); + + for (int i = 0; i < 2; ++i) { + ASSERT_TRUE(content_encodings[i]); + EXPECT_EQ(i, content_encodings[i]->order()); + EXPECT_EQ(ContentEncoding::kScopeAllFrameContents | + ContentEncoding::kScopeTrackPrivateData, + content_encodings[i]->scope()); + EXPECT_EQ(ContentEncoding::kTypeEncryption, content_encodings[i]->type()); + EXPECT_EQ(!i ? ContentEncoding::kEncAlgoAes : ContentEncoding::kEncAlgoDes, + content_encodings[i]->encryption_algo()); + EXPECT_EQ(8u, content_encodings[i]->encryption_key_id().size()); + } +} + +TEST_F(WebMContentEncodingsClientTest, DefaultValues) { + const uint8 kContentEncodings[] = { + 0x6D, 0x80, 0x8A, // ContentEncodings (size = 10) + 0x62, 0x40, 0x87, // ContentEncoding (size = 7) + // ContentEncodingOrder missing + // ContentEncodingScope missing + 0x50, 0x33, 0x81, 0x01, // ContentEncodingType (size = 1) + 0x50, 0x35, 0x80, // ContentEncryption (size = 0) + // ContentEncAlgo missing + }; + int size = sizeof(kContentEncodings); + + int result = parser_.Parse(kContentEncodings, size); + ASSERT_EQ(size, result); + const ContentEncodings& content_encodings = client_.content_encodings(); + + ASSERT_EQ(1u, content_encodings.size()); + ASSERT_TRUE(content_encodings[0]); + EXPECT_EQ(0, content_encodings[0]->order()); + EXPECT_EQ(ContentEncoding::kScopeAllFrameContents, + content_encodings[0]->scope()); + EXPECT_EQ(ContentEncoding::kTypeEncryption, content_encodings[0]->type()); + EXPECT_EQ(ContentEncoding::kEncAlgoNotEncrypted, + content_encodings[0]->encryption_algo()); + EXPECT_TRUE(content_encodings[0]->encryption_key_id().empty()); +} + +TEST_F(WebMContentEncodingsClientTest, ContentEncodingsClientReuse) { + const uint8 kContentEncodings[] = { + 0x6D, 0x80, 0xA1, // ContentEncodings (size = 33) + 0x62, 0x40, 0x9e, // ContentEncoding (size = 30) + 0x50, 0x31, 0x81, 0x00, // ContentEncodingOrder (size = 1) + 0x50, 0x32, 0x81, 0x01, // ContentEncodingScope (size = 1) + 0x50, 0x33, 0x81, 0x01, // ContentEncodingType (size = 1) + 0x50, 0x35, 0x8F, // ContentEncryption (size = 15) + 0x47, 0xE1, 0x81, 0x05, // ContentEncAlgo (size = 1) + 0x47, 0xE2, 0x88, // ContentEncKeyID (size = 8) + 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, + }; + int size = sizeof(kContentEncodings); + + // Parse for the first time. + int result = parser_.Parse(kContentEncodings, size); + ASSERT_EQ(size, result); + + // Parse again. + parser_.Reset(); + result = parser_.Parse(kContentEncodings, size); + ASSERT_EQ(size, result); + const ContentEncodings& content_encodings = client_.content_encodings(); + + ASSERT_EQ(1u, content_encodings.size()); + ASSERT_TRUE(content_encodings[0]); + EXPECT_EQ(0, content_encodings[0]->order()); + EXPECT_EQ(ContentEncoding::kScopeAllFrameContents, + content_encodings[0]->scope()); + EXPECT_EQ(ContentEncoding::kTypeEncryption, content_encodings[0]->type()); + EXPECT_EQ(ContentEncoding::kEncAlgoAes, + content_encodings[0]->encryption_algo()); + EXPECT_EQ(8u, content_encodings[0]->encryption_key_id().size()); +} + +TEST_F(WebMContentEncodingsClientTest, InvalidContentEncodingOrder) { + const uint8 kContentEncodings[] = { + 0x6D, 0x80, 0x8E, // ContentEncodings (size = 14) + 0x62, 0x40, 0x8B, // ContentEncoding (size = 11) + 0x50, 0x31, 0x81, 0xEE, // ContentEncodingOrder (size = 1), invalid + 0x50, 0x33, 0x81, 0x01, // ContentEncodingType (size = 1) + 0x50, 0x35, 0x80, // ContentEncryption (size = 0) + }; + int size = sizeof(kContentEncodings); + EXPECT_MEDIA_LOG(UnexpectedContentEncodingOrder()); + ParseAndExpectToFail(kContentEncodings, size); +} + +TEST_F(WebMContentEncodingsClientTest, InvalidContentEncodingScope) { + const uint8 kContentEncodings[] = { + 0x6D, 0x80, 0x8E, // ContentEncodings (size = 14) + 0x62, 0x40, 0x8B, // ContentEncoding (size = 11) + 0x50, 0x32, 0x81, 0xEE, // ContentEncodingScope (size = 1), invalid + 0x50, 0x33, 0x81, 0x01, // ContentEncodingType (size = 1) + 0x50, 0x35, 0x80, // ContentEncryption (size = 0) + }; + int size = sizeof(kContentEncodings); + EXPECT_MEDIA_LOG(UnexpectedContentEncodingScope()); + ParseAndExpectToFail(kContentEncodings, size); +} + +TEST_F(WebMContentEncodingsClientTest, InvalidContentEncodingType) { + const uint8 kContentEncodings[] = { + 0x6D, 0x80, 0x8E, // ContentEncodings (size = 14) + 0x62, 0x40, 0x8B, // ContentEncoding (size = 11) + 0x50, 0x33, 0x81, 0x00, // ContentEncodingType (size = 1), invalid + 0x50, 0x35, 0x80, // ContentEncryption (size = 0) + }; + int size = sizeof(kContentEncodings); + EXPECT_MEDIA_LOG(ContentCompressionNotSupported()); + ParseAndExpectToFail(kContentEncodings, size); +} + +// ContentEncodingType is encryption but no ContentEncryption present. +TEST_F(WebMContentEncodingsClientTest, MissingContentEncryption) { + const uint8 kContentEncodings[] = { + 0x6D, 0x80, 0x87, // ContentEncodings (size = 7) + 0x62, 0x40, 0x84, // ContentEncoding (size = 4) + 0x50, 0x33, 0x81, 0x01, // ContentEncodingType (size = 1) + // ContentEncryption missing + }; + int size = sizeof(kContentEncodings); + EXPECT_MEDIA_LOG(MissingContentEncryption()); + ParseAndExpectToFail(kContentEncodings, size); +} + +TEST_F(WebMContentEncodingsClientTest, InvalidContentEncAlgo) { + const uint8 kContentEncodings[] = { + 0x6D, 0x80, 0x99, // ContentEncodings (size = 25) + 0x62, 0x40, 0x96, // ContentEncoding (size = 22) + 0x50, 0x33, 0x81, 0x01, // ContentEncodingType (size = 1) + 0x50, 0x35, 0x8F, // ContentEncryption (size = 15) + 0x47, 0xE1, 0x81, 0xEE, // ContentEncAlgo (size = 1), invalid + 0x47, 0xE2, 0x88, // ContentEncKeyID (size = 8) + 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, + }; + int size = sizeof(kContentEncodings); + EXPECT_MEDIA_LOG(UnexpectedContentEncAlgo(0xEE)); + ParseAndExpectToFail(kContentEncodings, size); +} + +} // namespace media diff --git a/packager/media/formats/webm/webm_crypto_helpers.cc b/packager/media/formats/webm/webm_crypto_helpers.cc new file mode 100644 index 0000000000..bd473bc23e --- /dev/null +++ b/packager/media/formats/webm/webm_crypto_helpers.cc @@ -0,0 +1,62 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "media/formats/webm/webm_crypto_helpers.h" + +#include "base/logging.h" +#include "base/sys_byteorder.h" +#include "media/base/decrypt_config.h" +#include "media/formats/webm/webm_constants.h" + +namespace media { +namespace { + +// Generates a 16 byte CTR counter block. The CTR counter block format is a +// CTR IV appended with a CTR block counter. |iv| is an 8 byte CTR IV. +// |iv_size| is the size of |iv| in btyes. Returns a string of +// kDecryptionKeySize bytes. +std::string GenerateWebMCounterBlock(const uint8* iv, int iv_size) { + std::string counter_block(reinterpret_cast(iv), iv_size); + counter_block.append(DecryptConfig::kDecryptionKeySize - iv_size, 0); + return counter_block; +} + +} // namespace anonymous + +bool WebMCreateDecryptConfig(const uint8* data, int data_size, + const uint8* key_id, int key_id_size, + scoped_ptr* decrypt_config, + int* data_offset) { + if (data_size < kWebMSignalByteSize) { + DVLOG(1) << "Got a block from an encrypted stream with no data."; + return false; + } + + uint8 signal_byte = data[0]; + int frame_offset = sizeof(signal_byte); + + // Setting the DecryptConfig object of the buffer while leaving the + // initialization vector empty will tell the decryptor that the frame is + // unencrypted. + std::string counter_block; + + if (signal_byte & kWebMFlagEncryptedFrame) { + if (data_size < kWebMSignalByteSize + kWebMIvSize) { + DVLOG(1) << "Got an encrypted block with not enough data " << data_size; + return false; + } + counter_block = GenerateWebMCounterBlock(data + frame_offset, kWebMIvSize); + frame_offset += kWebMIvSize; + } + + decrypt_config->reset(new DecryptConfig( + std::string(reinterpret_cast(key_id), key_id_size), + counter_block, + std::vector())); + *data_offset = frame_offset; + + return true; +} + +} // namespace media diff --git a/packager/media/formats/webm/webm_crypto_helpers.h b/packager/media/formats/webm/webm_crypto_helpers.h new file mode 100644 index 0000000000..41ad5b13ed --- /dev/null +++ b/packager/media/formats/webm/webm_crypto_helpers.h @@ -0,0 +1,28 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef MEDIA_FORMATS_WEBM_WEBM_CRYPTO_HELPERS_H_ +#define MEDIA_FORMATS_WEBM_WEBM_CRYPTO_HELPERS_H_ + +#include "base/basictypes.h" +#include "base/memory/scoped_ptr.h" +#include "media/base/decoder_buffer.h" + +namespace media { + +// Fills an initialized DecryptConfig, which can be sent to the Decryptor if +// the stream has potentially encrypted frames. Also sets |data_offset| which +// indicates where the encrypted data starts. Leaving the IV empty will tell +// the decryptor that the frame is unencrypted. Returns true if |data| is valid, +// false otherwise, in which case |decrypt_config| and |data_offset| will not be +// changed. Current encrypted WebM request for comments specification is here +// http://wiki.webmproject.org/encryption/webm-encryption-rfc +bool WebMCreateDecryptConfig(const uint8* data, int data_size, + const uint8* key_id, int key_id_size, + scoped_ptr* decrypt_config, + int* data_offset); + +} // namespace media + +#endif // MEDIA_FORMATS_WEBM_WEBM_CRYPT_HELPERS_H_ diff --git a/packager/media/formats/webm/webm_info_parser.cc b/packager/media/formats/webm/webm_info_parser.cc new file mode 100644 index 0000000000..6309c21e9d --- /dev/null +++ b/packager/media/formats/webm/webm_info_parser.cc @@ -0,0 +1,103 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "media/formats/webm/webm_info_parser.h" + +#include "base/logging.h" +#include "media/formats/webm/webm_constants.h" + +namespace media { + +// Default timecode scale if the TimecodeScale element is +// not specified in the INFO element. +static const int kWebMDefaultTimecodeScale = 1000000; + +WebMInfoParser::WebMInfoParser() + : timecode_scale_(-1), + duration_(-1) { +} + +WebMInfoParser::~WebMInfoParser() {} + +int WebMInfoParser::Parse(const uint8* buf, int size) { + timecode_scale_ = -1; + duration_ = -1; + + WebMListParser parser(kWebMIdInfo, this); + int result = parser.Parse(buf, size); + + if (result <= 0) + return result; + + // For now we do all or nothing parsing. + return parser.IsParsingComplete() ? result : 0; +} + +WebMParserClient* WebMInfoParser::OnListStart(int id) { return this; } + +bool WebMInfoParser::OnListEnd(int id) { + if (id == kWebMIdInfo && timecode_scale_ == -1) { + // Set timecode scale to default value if it isn't present in + // the Info element. + timecode_scale_ = kWebMDefaultTimecodeScale; + } + return true; +} + +bool WebMInfoParser::OnUInt(int id, int64 val) { + if (id != kWebMIdTimecodeScale) + return true; + + if (timecode_scale_ != -1) { + DVLOG(1) << "Multiple values for id " << std::hex << id << " specified"; + return false; + } + + timecode_scale_ = val; + return true; +} + +bool WebMInfoParser::OnFloat(int id, double val) { + if (id != kWebMIdDuration) { + DVLOG(1) << "Unexpected float for id" << std::hex << id; + return false; + } + + if (duration_ != -1) { + DVLOG(1) << "Multiple values for duration."; + return false; + } + + duration_ = val; + return true; +} + +bool WebMInfoParser::OnBinary(int id, const uint8* data, int size) { + if (id == kWebMIdDateUTC) { + if (size != 8) + return false; + + int64 date_in_nanoseconds = 0; + for (int i = 0; i < size; ++i) + date_in_nanoseconds = (date_in_nanoseconds << 8) | data[i]; + + base::Time::Exploded exploded_epoch; + exploded_epoch.year = 2001; + exploded_epoch.month = 1; + exploded_epoch.day_of_month = 1; + exploded_epoch.hour = 0; + exploded_epoch.minute = 0; + exploded_epoch.second = 0; + exploded_epoch.millisecond = 0; + date_utc_ = base::Time::FromUTCExploded(exploded_epoch) + + base::TimeDelta::FromMicroseconds(date_in_nanoseconds / 1000); + } + return true; +} + +bool WebMInfoParser::OnString(int id, const std::string& str) { + return true; +} + +} // namespace media diff --git a/packager/media/formats/webm/webm_info_parser.h b/packager/media/formats/webm/webm_info_parser.h new file mode 100644 index 0000000000..44ef6b2074 --- /dev/null +++ b/packager/media/formats/webm/webm_info_parser.h @@ -0,0 +1,50 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef MEDIA_FORMATS_WEBM_WEBM_INFO_PARSER_H_ +#define MEDIA_FORMATS_WEBM_WEBM_INFO_PARSER_H_ + +#include "base/compiler_specific.h" +#include "base/time/time.h" +#include "media/base/media_export.h" +#include "media/formats/webm/webm_parser.h" + +namespace media { + +// Parser for WebM Info element. +class MEDIA_EXPORT WebMInfoParser : public WebMParserClient { + public: + WebMInfoParser(); + ~WebMInfoParser() override; + + // Parses a WebM Info element in |buf|. + // + // Returns -1 if the parse fails. + // Returns 0 if more data is needed. + // Returns the number of bytes parsed on success. + int Parse(const uint8* buf, int size); + + int64 timecode_scale() const { return timecode_scale_; } + double duration() const { return duration_; } + base::Time date_utc() const { return date_utc_; } + + private: + // WebMParserClient methods + WebMParserClient* OnListStart(int id) override; + bool OnListEnd(int id) override; + bool OnUInt(int id, int64 val) override; + bool OnFloat(int id, double val) override; + bool OnBinary(int id, const uint8* data, int size) override; + bool OnString(int id, const std::string& str) override; + + int64 timecode_scale_; + double duration_; + base::Time date_utc_; + + DISALLOW_COPY_AND_ASSIGN(WebMInfoParser); +}; + +} // namespace media + +#endif // MEDIA_FORMATS_WEBM_WEBM_INFO_PARSER_H_ diff --git a/packager/media/formats/webm/webm_parser.cc b/packager/media/formats/webm/webm_parser.cc new file mode 100644 index 0000000000..fda287c0d9 --- /dev/null +++ b/packager/media/formats/webm/webm_parser.cc @@ -0,0 +1,954 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "media/formats/webm/webm_parser.h" + +// This file contains code to parse WebM file elements. It was created +// from information in the Matroska spec. +// http://www.matroska.org/technical/specs/index.html +// This file contains code for encrypted WebM. Current WebM +// encrypted request for comments specification is here +// http://wiki.webmproject.org/encryption/webm-encryption-rfc + +#include + +#include "base/logging.h" +#include "base/numerics/safe_conversions.h" +#include "media/formats/webm/webm_constants.h" + +namespace media { + +enum ElementType { + UNKNOWN, + LIST, // Referred to as Master Element in the Matroska spec. + UINT, + FLOAT, + BINARY, + STRING, + SKIP, +}; + +struct ElementIdInfo { + ElementType type_; + int id_; +}; + +struct ListElementInfo { + int id_; + int level_; + const ElementIdInfo* id_info_; + int id_info_count_; +}; + +// The following are tables indicating what IDs are valid sub-elements +// of particular elements. If an element is encountered that doesn't +// appear in the list, a parsing error is signalled. Some elements are +// marked as SKIP because they are valid, but we don't care about them +// right now. +static const ElementIdInfo kEBMLHeaderIds[] = { + {UINT, kWebMIdEBMLVersion}, + {UINT, kWebMIdEBMLReadVersion}, + {UINT, kWebMIdEBMLMaxIDLength}, + {UINT, kWebMIdEBMLMaxSizeLength}, + {STRING, kWebMIdDocType}, + {UINT, kWebMIdDocTypeVersion}, + {UINT, kWebMIdDocTypeReadVersion}, +}; + +static const ElementIdInfo kSegmentIds[] = { + {LIST, kWebMIdSeekHead}, + {LIST, kWebMIdInfo}, + {LIST, kWebMIdCluster}, + {LIST, kWebMIdTracks}, + {LIST, kWebMIdCues}, + {LIST, kWebMIdAttachments}, + {LIST, kWebMIdChapters}, + {LIST, kWebMIdTags}, +}; + +static const ElementIdInfo kSeekHeadIds[] = { + {LIST, kWebMIdSeek}, +}; + +static const ElementIdInfo kSeekIds[] = { + {BINARY, kWebMIdSeekID}, + {UINT, kWebMIdSeekPosition}, +}; + +static const ElementIdInfo kInfoIds[] = { + {BINARY, kWebMIdSegmentUID}, + {STRING, kWebMIdSegmentFilename}, + {BINARY, kWebMIdPrevUID}, + {STRING, kWebMIdPrevFilename}, + {BINARY, kWebMIdNextUID}, + {STRING, kWebMIdNextFilename}, + {BINARY, kWebMIdSegmentFamily}, + {LIST, kWebMIdChapterTranslate}, + {UINT, kWebMIdTimecodeScale}, + {FLOAT, kWebMIdDuration}, + {BINARY, kWebMIdDateUTC}, + {STRING, kWebMIdTitle}, + {STRING, kWebMIdMuxingApp}, + {STRING, kWebMIdWritingApp}, +}; + +static const ElementIdInfo kChapterTranslateIds[] = { + {UINT, kWebMIdChapterTranslateEditionUID}, + {UINT, kWebMIdChapterTranslateCodec}, + {BINARY, kWebMIdChapterTranslateID}, +}; + +static const ElementIdInfo kClusterIds[] = { + {BINARY, kWebMIdSimpleBlock}, + {UINT, kWebMIdTimecode}, + {LIST, kWebMIdSilentTracks}, + {UINT, kWebMIdPosition}, + {UINT, kWebMIdPrevSize}, + {LIST, kWebMIdBlockGroup}, +}; + +static const ElementIdInfo kSilentTracksIds[] = { + {UINT, kWebMIdSilentTrackNumber}, +}; + +static const ElementIdInfo kBlockGroupIds[] = { + {BINARY, kWebMIdBlock}, + {LIST, kWebMIdBlockAdditions}, + {UINT, kWebMIdBlockDuration}, + {UINT, kWebMIdReferencePriority}, + {BINARY, kWebMIdReferenceBlock}, + {BINARY, kWebMIdCodecState}, + {BINARY, kWebMIdDiscardPadding}, + {LIST, kWebMIdSlices}, +}; + +static const ElementIdInfo kBlockAdditionsIds[] = { + {LIST, kWebMIdBlockMore}, +}; + +static const ElementIdInfo kBlockMoreIds[] = { + {UINT, kWebMIdBlockAddID}, + {BINARY, kWebMIdBlockAdditional}, +}; + +static const ElementIdInfo kSlicesIds[] = { + {LIST, kWebMIdTimeSlice}, +}; + +static const ElementIdInfo kTimeSliceIds[] = { + {UINT, kWebMIdLaceNumber}, +}; + +static const ElementIdInfo kTracksIds[] = { + {LIST, kWebMIdTrackEntry}, +}; + +static const ElementIdInfo kTrackEntryIds[] = { + {UINT, kWebMIdTrackNumber}, + {BINARY, kWebMIdTrackUID}, + {UINT, kWebMIdTrackType}, + {UINT, kWebMIdFlagEnabled}, + {UINT, kWebMIdFlagDefault}, + {UINT, kWebMIdFlagForced}, + {UINT, kWebMIdFlagLacing}, + {UINT, kWebMIdMinCache}, + {UINT, kWebMIdMaxCache}, + {UINT, kWebMIdDefaultDuration}, + {FLOAT, kWebMIdTrackTimecodeScale}, + {UINT, kWebMIdMaxBlockAdditionId}, + {STRING, kWebMIdName}, + {STRING, kWebMIdLanguage}, + {STRING, kWebMIdCodecID}, + {BINARY, kWebMIdCodecPrivate}, + {STRING, kWebMIdCodecName}, + {UINT, kWebMIdAttachmentLink}, + {UINT, kWebMIdCodecDecodeAll}, + {UINT, kWebMIdTrackOverlay}, + {UINT, kWebMIdCodecDelay}, + {UINT, kWebMIdSeekPreRoll}, + {LIST, kWebMIdTrackTranslate}, + {LIST, kWebMIdVideo}, + {LIST, kWebMIdAudio}, + {LIST, kWebMIdTrackOperation}, + {LIST, kWebMIdContentEncodings}, +}; + +static const ElementIdInfo kTrackTranslateIds[] = { + {UINT, kWebMIdTrackTranslateEditionUID}, + {UINT, kWebMIdTrackTranslateCodec}, + {BINARY, kWebMIdTrackTranslateTrackID}, +}; + +static const ElementIdInfo kVideoIds[] = { + {UINT, kWebMIdFlagInterlaced}, + {UINT, kWebMIdStereoMode}, + {UINT, kWebMIdAlphaMode}, + {UINT, kWebMIdPixelWidth}, + {UINT, kWebMIdPixelHeight}, + {UINT, kWebMIdPixelCropBottom}, + {UINT, kWebMIdPixelCropTop}, + {UINT, kWebMIdPixelCropLeft}, + {UINT, kWebMIdPixelCropRight}, + {UINT, kWebMIdDisplayWidth}, + {UINT, kWebMIdDisplayHeight}, + {UINT, kWebMIdDisplayUnit}, + {UINT, kWebMIdAspectRatioType}, + {BINARY, kWebMIdColorSpace}, + {FLOAT, kWebMIdFrameRate}, +}; + +static const ElementIdInfo kAudioIds[] = { + {FLOAT, kWebMIdSamplingFrequency}, + {FLOAT, kWebMIdOutputSamplingFrequency}, + {UINT, kWebMIdChannels}, + {UINT, kWebMIdBitDepth}, +}; + +static const ElementIdInfo kTrackOperationIds[] = { + {LIST, kWebMIdTrackCombinePlanes}, + {LIST, kWebMIdJoinBlocks}, +}; + +static const ElementIdInfo kTrackCombinePlanesIds[] = { + {LIST, kWebMIdTrackPlane}, +}; + +static const ElementIdInfo kTrackPlaneIds[] = { + {UINT, kWebMIdTrackPlaneUID}, + {UINT, kWebMIdTrackPlaneType}, +}; + +static const ElementIdInfo kJoinBlocksIds[] = { + {UINT, kWebMIdTrackJoinUID}, +}; + +static const ElementIdInfo kContentEncodingsIds[] = { + {LIST, kWebMIdContentEncoding}, +}; + +static const ElementIdInfo kContentEncodingIds[] = { + {UINT, kWebMIdContentEncodingOrder}, + {UINT, kWebMIdContentEncodingScope}, + {UINT, kWebMIdContentEncodingType}, + {LIST, kWebMIdContentCompression}, + {LIST, kWebMIdContentEncryption}, +}; + +static const ElementIdInfo kContentCompressionIds[] = { + {UINT, kWebMIdContentCompAlgo}, + {BINARY, kWebMIdContentCompSettings}, +}; + +static const ElementIdInfo kContentEncryptionIds[] = { + {LIST, kWebMIdContentEncAESSettings}, + {UINT, kWebMIdContentEncAlgo}, + {BINARY, kWebMIdContentEncKeyID}, + {BINARY, kWebMIdContentSignature}, + {BINARY, kWebMIdContentSigKeyID}, + {UINT, kWebMIdContentSigAlgo}, + {UINT, kWebMIdContentSigHashAlgo}, +}; + +static const ElementIdInfo kContentEncAESSettingsIds[] = { + {UINT, kWebMIdAESSettingsCipherMode}, +}; + +static const ElementIdInfo kCuesIds[] = { + {LIST, kWebMIdCuePoint}, +}; + +static const ElementIdInfo kCuePointIds[] = { + {UINT, kWebMIdCueTime}, + {LIST, kWebMIdCueTrackPositions}, +}; + +static const ElementIdInfo kCueTrackPositionsIds[] = { + {UINT, kWebMIdCueTrack}, + {UINT, kWebMIdCueClusterPosition}, + {UINT, kWebMIdCueBlockNumber}, + {UINT, kWebMIdCueCodecState}, + {LIST, kWebMIdCueReference}, +}; + +static const ElementIdInfo kCueReferenceIds[] = { + {UINT, kWebMIdCueRefTime}, +}; + +static const ElementIdInfo kAttachmentsIds[] = { + {LIST, kWebMIdAttachedFile}, +}; + +static const ElementIdInfo kAttachedFileIds[] = { + {STRING, kWebMIdFileDescription}, + {STRING, kWebMIdFileName}, + {STRING, kWebMIdFileMimeType}, + {BINARY, kWebMIdFileData}, + {UINT, kWebMIdFileUID}, +}; + +static const ElementIdInfo kChaptersIds[] = { + {LIST, kWebMIdEditionEntry}, +}; + +static const ElementIdInfo kEditionEntryIds[] = { + {UINT, kWebMIdEditionUID}, + {UINT, kWebMIdEditionFlagHidden}, + {UINT, kWebMIdEditionFlagDefault}, + {UINT, kWebMIdEditionFlagOrdered}, + {LIST, kWebMIdChapterAtom}, +}; + +static const ElementIdInfo kChapterAtomIds[] = { + {UINT, kWebMIdChapterUID}, + {UINT, kWebMIdChapterTimeStart}, + {UINT, kWebMIdChapterTimeEnd}, + {UINT, kWebMIdChapterFlagHidden}, + {UINT, kWebMIdChapterFlagEnabled}, + {BINARY, kWebMIdChapterSegmentUID}, + {UINT, kWebMIdChapterSegmentEditionUID}, + {UINT, kWebMIdChapterPhysicalEquiv}, + {LIST, kWebMIdChapterTrack}, + {LIST, kWebMIdChapterDisplay}, + {LIST, kWebMIdChapProcess}, +}; + +static const ElementIdInfo kChapterTrackIds[] = { + {UINT, kWebMIdChapterTrackNumber}, +}; + +static const ElementIdInfo kChapterDisplayIds[] = { + {STRING, kWebMIdChapString}, + {STRING, kWebMIdChapLanguage}, + {STRING, kWebMIdChapCountry}, +}; + +static const ElementIdInfo kChapProcessIds[] = { + {UINT, kWebMIdChapProcessCodecID}, + {BINARY, kWebMIdChapProcessPrivate}, + {LIST, kWebMIdChapProcessCommand}, +}; + +static const ElementIdInfo kChapProcessCommandIds[] = { + {UINT, kWebMIdChapProcessTime}, + {BINARY, kWebMIdChapProcessData}, +}; + +static const ElementIdInfo kTagsIds[] = { + {LIST, kWebMIdTag}, +}; + +static const ElementIdInfo kTagIds[] = { + {LIST, kWebMIdTargets}, + {LIST, kWebMIdSimpleTag}, +}; + +static const ElementIdInfo kTargetsIds[] = { + {UINT, kWebMIdTargetTypeValue}, + {STRING, kWebMIdTargetType}, + {UINT, kWebMIdTagTrackUID}, + {UINT, kWebMIdTagEditionUID}, + {UINT, kWebMIdTagChapterUID}, + {UINT, kWebMIdTagAttachmentUID}, +}; + +static const ElementIdInfo kSimpleTagIds[] = { + {STRING, kWebMIdTagName}, + {STRING, kWebMIdTagLanguage}, + {UINT, kWebMIdTagDefault}, + {STRING, kWebMIdTagString}, + {BINARY, kWebMIdTagBinary}, +}; + +#define LIST_ELEMENT_INFO(id, level, id_info) \ + { (id), (level), (id_info), arraysize(id_info) } + +static const ListElementInfo kListElementInfo[] = { + LIST_ELEMENT_INFO(kWebMIdCluster, 1, kClusterIds), + LIST_ELEMENT_INFO(kWebMIdEBMLHeader, 0, kEBMLHeaderIds), + LIST_ELEMENT_INFO(kWebMIdSegment, 0, kSegmentIds), + LIST_ELEMENT_INFO(kWebMIdSeekHead, 1, kSeekHeadIds), + LIST_ELEMENT_INFO(kWebMIdSeek, 2, kSeekIds), + LIST_ELEMENT_INFO(kWebMIdInfo, 1, kInfoIds), + LIST_ELEMENT_INFO(kWebMIdChapterTranslate, 2, kChapterTranslateIds), + LIST_ELEMENT_INFO(kWebMIdSilentTracks, 2, kSilentTracksIds), + LIST_ELEMENT_INFO(kWebMIdBlockGroup, 2, kBlockGroupIds), + LIST_ELEMENT_INFO(kWebMIdBlockAdditions, 3, kBlockAdditionsIds), + LIST_ELEMENT_INFO(kWebMIdBlockMore, 4, kBlockMoreIds), + LIST_ELEMENT_INFO(kWebMIdSlices, 3, kSlicesIds), + LIST_ELEMENT_INFO(kWebMIdTimeSlice, 4, kTimeSliceIds), + LIST_ELEMENT_INFO(kWebMIdTracks, 1, kTracksIds), + LIST_ELEMENT_INFO(kWebMIdTrackEntry, 2, kTrackEntryIds), + LIST_ELEMENT_INFO(kWebMIdTrackTranslate, 3, kTrackTranslateIds), + LIST_ELEMENT_INFO(kWebMIdVideo, 3, kVideoIds), + LIST_ELEMENT_INFO(kWebMIdAudio, 3, kAudioIds), + LIST_ELEMENT_INFO(kWebMIdTrackOperation, 3, kTrackOperationIds), + LIST_ELEMENT_INFO(kWebMIdTrackCombinePlanes, 4, kTrackCombinePlanesIds), + LIST_ELEMENT_INFO(kWebMIdTrackPlane, 5, kTrackPlaneIds), + LIST_ELEMENT_INFO(kWebMIdJoinBlocks, 4, kJoinBlocksIds), + LIST_ELEMENT_INFO(kWebMIdContentEncodings, 3, kContentEncodingsIds), + LIST_ELEMENT_INFO(kWebMIdContentEncoding, 4, kContentEncodingIds), + LIST_ELEMENT_INFO(kWebMIdContentCompression, 5, kContentCompressionIds), + LIST_ELEMENT_INFO(kWebMIdContentEncryption, 5, kContentEncryptionIds), + LIST_ELEMENT_INFO(kWebMIdContentEncAESSettings, 6, kContentEncAESSettingsIds), + LIST_ELEMENT_INFO(kWebMIdCues, 1, kCuesIds), + LIST_ELEMENT_INFO(kWebMIdCuePoint, 2, kCuePointIds), + LIST_ELEMENT_INFO(kWebMIdCueTrackPositions, 3, kCueTrackPositionsIds), + LIST_ELEMENT_INFO(kWebMIdCueReference, 4, kCueReferenceIds), + LIST_ELEMENT_INFO(kWebMIdAttachments, 1, kAttachmentsIds), + LIST_ELEMENT_INFO(kWebMIdAttachedFile, 2, kAttachedFileIds), + LIST_ELEMENT_INFO(kWebMIdChapters, 1, kChaptersIds), + LIST_ELEMENT_INFO(kWebMIdEditionEntry, 2, kEditionEntryIds), + LIST_ELEMENT_INFO(kWebMIdChapterAtom, 3, kChapterAtomIds), + LIST_ELEMENT_INFO(kWebMIdChapterTrack, 4, kChapterTrackIds), + LIST_ELEMENT_INFO(kWebMIdChapterDisplay, 4, kChapterDisplayIds), + LIST_ELEMENT_INFO(kWebMIdChapProcess, 4, kChapProcessIds), + LIST_ELEMENT_INFO(kWebMIdChapProcessCommand, 5, kChapProcessCommandIds), + LIST_ELEMENT_INFO(kWebMIdTags, 1, kTagsIds), + LIST_ELEMENT_INFO(kWebMIdTag, 2, kTagIds), + LIST_ELEMENT_INFO(kWebMIdTargets, 3, kTargetsIds), + LIST_ELEMENT_INFO(kWebMIdSimpleTag, 3, kSimpleTagIds), +}; + +// Parses an element header id or size field. These fields are variable length +// encoded. The first byte indicates how many bytes the field occupies. +// |buf| - The buffer to parse. +// |size| - The number of bytes in |buf| +// |max_bytes| - The maximum number of bytes the field can be. ID fields +// set this to 4 & element size fields set this to 8. If the +// first byte indicates a larger field size than this it is a +// parser error. +// |mask_first_byte| - For element size fields the field length encoding bits +// need to be masked off. This parameter is true for +// element size fields and is false for ID field values. +// +// Returns: The number of bytes parsed on success. -1 on error. +static int ParseWebMElementHeaderField(const uint8* buf, int size, + int max_bytes, bool mask_first_byte, + int64* num) { + DCHECK(buf); + DCHECK(num); + + if (size < 0) + return -1; + + if (size == 0) + return 0; + + int mask = 0x80; + uint8 ch = buf[0]; + int extra_bytes = -1; + bool all_ones = false; + for (int i = 0; i < max_bytes; ++i) { + if ((ch & mask) != 0) { + mask = ~mask & 0xff; + *num = mask_first_byte ? ch & mask : ch; + all_ones = (ch & mask) == mask; + extra_bytes = i; + break; + } + mask = 0x80 | mask >> 1; + } + + if (extra_bytes == -1) + return -1; + + // Return 0 if we need more data. + if ((1 + extra_bytes) > size) + return 0; + + int bytes_used = 1; + + for (int i = 0; i < extra_bytes; ++i) { + ch = buf[bytes_used++]; + all_ones &= (ch == 0xff); + *num = (*num << 8) | ch; + } + + if (all_ones) + *num = kint64max; + + return bytes_used; +} + +int WebMParseElementHeader(const uint8* buf, int size, + int* id, int64* element_size) { + DCHECK(buf); + DCHECK_GE(size, 0); + DCHECK(id); + DCHECK(element_size); + + if (size == 0) + return 0; + + int64 tmp = 0; + int num_id_bytes = ParseWebMElementHeaderField(buf, size, 4, false, &tmp); + + if (num_id_bytes <= 0) + return num_id_bytes; + + if (tmp == kint64max) + tmp = kWebMReservedId; + + *id = static_cast(tmp); + + int num_size_bytes = ParseWebMElementHeaderField(buf + num_id_bytes, + size - num_id_bytes, + 8, true, &tmp); + + if (num_size_bytes <= 0) + return num_size_bytes; + + if (tmp == kint64max) + tmp = kWebMUnknownSize; + + *element_size = tmp; + DVLOG(3) << "WebMParseElementHeader() : id " << std::hex << *id << std::dec + << " size " << *element_size; + return num_id_bytes + num_size_bytes; +} + +// Finds ElementType for a specific ID. +static ElementType FindIdType(int id, + const ElementIdInfo* id_info, + int id_info_count) { + + // Check for global element IDs that can be anywhere. + if (id == kWebMIdVoid || id == kWebMIdCRC32) + return SKIP; + + for (int i = 0; i < id_info_count; ++i) { + if (id == id_info[i].id_) + return id_info[i].type_; + } + + return UNKNOWN; +} + +// Finds ListElementInfo for a specific ID. +static const ListElementInfo* FindListInfo(int id) { + for (size_t i = 0; i < arraysize(kListElementInfo); ++i) { + if (id == kListElementInfo[i].id_) + return &kListElementInfo[i]; + } + + return NULL; +} + +static int FindListLevel(int id) { + const ListElementInfo* list_info = FindListInfo(id); + if (list_info) + return list_info->level_; + + return -1; +} + +static int ParseUInt(const uint8* buf, int size, int id, + WebMParserClient* client) { + if ((size <= 0) || (size > 8)) + return -1; + + // Read in the big-endian integer. + uint64 value = 0; + for (int i = 0; i < size; ++i) + value = (value << 8) | buf[i]; + + // We use int64 in place of uint64 everywhere for convenience. See this bug + // for more details: http://crbug.com/366750#c3 + if (!base::IsValueInRangeForNumericType(value)) + return -1; + + if (!client->OnUInt(id, value)) + return -1; + + return size; +} + +static int ParseFloat(const uint8* buf, int size, int id, + WebMParserClient* client) { + + if ((size != 4) && (size != 8)) + return -1; + + double value = -1; + + // Read the bytes from big-endian form into a native endian integer. + int64 tmp = 0; + for (int i = 0; i < size; ++i) + tmp = (tmp << 8) | buf[i]; + + // Use a union to convert the integer bit pattern into a floating point + // number. + if (size == 4) { + union { + int32 src; + float dst; + } tmp2; + tmp2.src = static_cast(tmp); + value = tmp2.dst; + } else if (size == 8) { + union { + int64 src; + double dst; + } tmp2; + tmp2.src = tmp; + value = tmp2.dst; + } else { + return -1; + } + + if (!client->OnFloat(id, value)) + return -1; + + return size; +} + +static int ParseBinary(const uint8* buf, int size, int id, + WebMParserClient* client) { + return client->OnBinary(id, buf, size) ? size : -1; +} + +static int ParseString(const uint8* buf, int size, int id, + WebMParserClient* client) { + const uint8* end = static_cast(memchr(buf, '\0', size)); + int length = (end != NULL) ? static_cast(end - buf) : size; + std::string str(reinterpret_cast(buf), length); + return client->OnString(id, str) ? size : -1; +} + +static int ParseNonListElement(ElementType type, int id, int64 element_size, + const uint8* buf, int size, + WebMParserClient* client) { + DCHECK_GE(size, element_size); + + int result = -1; + switch(type) { + case LIST: + NOTIMPLEMENTED(); + result = -1; + break; + case UINT: + result = ParseUInt(buf, element_size, id, client); + break; + case FLOAT: + result = ParseFloat(buf, element_size, id, client); + break; + case BINARY: + result = ParseBinary(buf, element_size, id, client); + break; + case STRING: + result = ParseString(buf, element_size, id, client); + break; + case SKIP: + result = element_size; + break; + default: + DVLOG(1) << "Unhandled ID type " << type; + return -1; + }; + + DCHECK_LE(result, size); + return result; +} + +WebMParserClient::WebMParserClient() {} +WebMParserClient::~WebMParserClient() {} + +WebMParserClient* WebMParserClient::OnListStart(int id) { + DVLOG(1) << "Unexpected list element start with ID " << std::hex << id; + return NULL; +} + +bool WebMParserClient::OnListEnd(int id) { + DVLOG(1) << "Unexpected list element end with ID " << std::hex << id; + return false; +} + +bool WebMParserClient::OnUInt(int id, int64 val) { + DVLOG(1) << "Unexpected unsigned integer element with ID " << std::hex << id; + return false; +} + +bool WebMParserClient::OnFloat(int id, double val) { + DVLOG(1) << "Unexpected float element with ID " << std::hex << id; + return false; +} + +bool WebMParserClient::OnBinary(int id, const uint8* data, int size) { + DVLOG(1) << "Unexpected binary element with ID " << std::hex << id; + return false; +} + +bool WebMParserClient::OnString(int id, const std::string& str) { + DVLOG(1) << "Unexpected string element with ID " << std::hex << id; + return false; +} + +WebMListParser::WebMListParser(int id, WebMParserClient* client) + : state_(NEED_LIST_HEADER), + root_id_(id), + root_level_(FindListLevel(id)), + root_client_(client) { + DCHECK_GE(root_level_, 0); + DCHECK(client); +} + +WebMListParser::~WebMListParser() {} + +void WebMListParser::Reset() { + ChangeState(NEED_LIST_HEADER); + list_state_stack_.clear(); +} + +int WebMListParser::Parse(const uint8* buf, int size) { + DCHECK(buf); + + if (size < 0 || state_ == PARSE_ERROR || state_ == DONE_PARSING_LIST) + return -1; + + if (size == 0) + return 0; + + const uint8* cur = buf; + int cur_size = size; + int bytes_parsed = 0; + + while (cur_size > 0 && state_ != PARSE_ERROR && state_ != DONE_PARSING_LIST) { + int element_id = 0; + int64 element_size = 0; + int result = WebMParseElementHeader(cur, cur_size, &element_id, + &element_size); + + if (result < 0) + return result; + + if (result == 0) + return bytes_parsed; + + switch(state_) { + case NEED_LIST_HEADER: { + if (element_id != root_id_) { + ChangeState(PARSE_ERROR); + return -1; + } + + // Only allow Segment & Cluster to have an unknown size. + if (element_size == kWebMUnknownSize && + (element_id != kWebMIdSegment) && + (element_id != kWebMIdCluster)) { + ChangeState(PARSE_ERROR); + return -1; + } + + ChangeState(INSIDE_LIST); + if (!OnListStart(root_id_, element_size)) + return -1; + + break; + } + + case INSIDE_LIST: { + int header_size = result; + const uint8* element_data = cur + header_size; + int element_data_size = cur_size - header_size; + + if (element_size < element_data_size) + element_data_size = element_size; + + result = ParseListElement(header_size, element_id, element_size, + element_data, element_data_size); + + DCHECK_LE(result, header_size + element_data_size); + if (result < 0) { + ChangeState(PARSE_ERROR); + return -1; + } + + if (result == 0) + return bytes_parsed; + + break; + } + case DONE_PARSING_LIST: + case PARSE_ERROR: + // Shouldn't be able to get here. + NOTIMPLEMENTED(); + break; + } + + cur += result; + cur_size -= result; + bytes_parsed += result; + } + + return (state_ == PARSE_ERROR) ? -1 : bytes_parsed; +} + +bool WebMListParser::IsParsingComplete() const { + return state_ == DONE_PARSING_LIST; +} + +void WebMListParser::ChangeState(State new_state) { + state_ = new_state; +} + +int WebMListParser::ParseListElement(int header_size, + int id, int64 element_size, + const uint8* data, int size) { + DCHECK_GT(list_state_stack_.size(), 0u); + + ListState& list_state = list_state_stack_.back(); + DCHECK(list_state.element_info_); + + const ListElementInfo* element_info = list_state.element_info_; + ElementType id_type = + FindIdType(id, element_info->id_info_, element_info->id_info_count_); + + // Unexpected ID. + if (id_type == UNKNOWN) { + if (list_state.size_ != kWebMUnknownSize || + !IsSiblingOrAncestor(list_state.id_, id)) { + DVLOG(1) << "No ElementType info for ID 0x" << std::hex << id; + return -1; + } + + // We've reached the end of a list of unknown size. Update the size now that + // we know it and dispatch the end of list calls. + list_state.size_ = list_state.bytes_parsed_; + + if (!OnListEnd()) + return -1; + + // Check to see if all open lists have ended. + if (list_state_stack_.size() == 0) + return 0; + + list_state = list_state_stack_.back(); + } + + // Make sure the whole element can fit inside the current list. + int64 total_element_size = header_size + element_size; + if (list_state.size_ != kWebMUnknownSize && + list_state.size_ < list_state.bytes_parsed_ + total_element_size) { + return -1; + } + + if (id_type == LIST) { + list_state.bytes_parsed_ += header_size; + + if (!OnListStart(id, element_size)) + return -1; + return header_size; + } + + // Make sure we have the entire element before trying to parse a non-list + // element. + if (size < element_size) + return 0; + + int bytes_parsed = ParseNonListElement(id_type, id, element_size, + data, size, list_state.client_); + DCHECK_LE(bytes_parsed, size); + + // Return if an error occurred or we need more data. + // Note: bytes_parsed is 0 for a successful parse of a size 0 element. We + // need to check the element_size to disambiguate the "need more data" case + // from a successful parse. + if (bytes_parsed < 0 || (bytes_parsed == 0 && element_size != 0)) + return bytes_parsed; + + int result = header_size + bytes_parsed; + list_state.bytes_parsed_ += result; + + // See if we have reached the end of the current list. + if (list_state.bytes_parsed_ == list_state.size_) { + if (!OnListEnd()) + return -1; + } + + return result; +} + +bool WebMListParser::OnListStart(int id, int64 size) { + const ListElementInfo* element_info = FindListInfo(id); + if (!element_info) + return false; + + int current_level = root_level_ + list_state_stack_.size() - 1; + if (current_level + 1 != element_info->level_) + return false; + + WebMParserClient* current_list_client = NULL; + if (!list_state_stack_.empty()) { + // Make sure the new list doesn't go past the end of the current list. + ListState current_list_state = list_state_stack_.back(); + if (current_list_state.size_ != kWebMUnknownSize && + current_list_state.size_ < current_list_state.bytes_parsed_ + size) + return false; + current_list_client = current_list_state.client_; + } else { + current_list_client = root_client_; + } + + WebMParserClient* new_list_client = current_list_client->OnListStart(id); + if (!new_list_client) + return false; + + ListState new_list_state = { id, size, 0, element_info, new_list_client }; + list_state_stack_.push_back(new_list_state); + + if (size == 0) + return OnListEnd(); + + return true; +} + +bool WebMListParser::OnListEnd() { + int lists_ended = 0; + for (; !list_state_stack_.empty(); ++lists_ended) { + const ListState& list_state = list_state_stack_.back(); + int64 bytes_parsed = list_state.bytes_parsed_; + int id = list_state.id_; + + if (bytes_parsed != list_state.size_) + break; + + list_state_stack_.pop_back(); + + WebMParserClient* client = NULL; + if (!list_state_stack_.empty()) { + // Update the bytes_parsed_ for the parent element. + list_state_stack_.back().bytes_parsed_ += bytes_parsed; + client = list_state_stack_.back().client_; + } else { + client = root_client_; + } + + if (!client->OnListEnd(id)) + return false; + } + + DCHECK_GE(lists_ended, 1); + + if (list_state_stack_.empty()) + ChangeState(DONE_PARSING_LIST); + + return true; +} + +bool WebMListParser::IsSiblingOrAncestor(int id_a, int id_b) const { + DCHECK((id_a == kWebMIdSegment) || (id_a == kWebMIdCluster)); + + if (id_a == kWebMIdCluster) { + // kWebMIdCluster siblings. + for (size_t i = 0; i < arraysize(kSegmentIds); i++) { + if (kSegmentIds[i].id_ == id_b) + return true; + } + } + + // kWebMIdSegment siblings. + return ((id_b == kWebMIdSegment) || (id_b == kWebMIdEBMLHeader)); +} + +} // namespace media diff --git a/packager/media/formats/webm/webm_parser.h b/packager/media/formats/webm/webm_parser.h new file mode 100644 index 0000000000..854db685f8 --- /dev/null +++ b/packager/media/formats/webm/webm_parser.h @@ -0,0 +1,158 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef MEDIA_FORMATS_WEBM_WEBM_PARSER_H_ +#define MEDIA_FORMATS_WEBM_WEBM_PARSER_H_ + +#include +#include + +#include "base/basictypes.h" +#include "media/base/media_export.h" + +namespace media { + +// Interface for receiving WebM parser events. +// +// Each method is called when an element of the specified type is parsed. +// The ID of the element that was parsed is given along with the value +// stored in the element. List elements generate calls at the start and +// end of the list. Any pointers passed to these methods are only guaranteed +// to be valid for the life of that call. Each method (except for OnListStart) +// returns a bool that indicates whether the parsed data is valid. OnListStart +// returns a pointer to a WebMParserClient object, which should be used to +// handle elements parsed out of the list being started. If false (or NULL by +// OnListStart) is returned then the parse is immediately terminated and an +// error is reported by the parser. +class MEDIA_EXPORT WebMParserClient { + public: + virtual ~WebMParserClient(); + + virtual WebMParserClient* OnListStart(int id); + virtual bool OnListEnd(int id); + virtual bool OnUInt(int id, int64 val); + virtual bool OnFloat(int id, double val); + virtual bool OnBinary(int id, const uint8* data, int size); + virtual bool OnString(int id, const std::string& str); + + protected: + WebMParserClient(); + + DISALLOW_COPY_AND_ASSIGN(WebMParserClient); +}; + +struct ListElementInfo; + +// Parses a WebM list element and all of its children. This +// class supports incremental parsing of the list so Parse() +// can be called multiple times with pieces of the list. +// IsParsingComplete() will return true once the entire list has +// been parsed. +class MEDIA_EXPORT WebMListParser { + public: + // |id| - Element ID of the list we intend to parse. + // |client| - Called as different elements in the list are parsed. + WebMListParser(int id, WebMParserClient* client); + ~WebMListParser(); + + // Resets the state of the parser so it can start parsing a new list. + void Reset(); + + // Parses list data contained in |buf|. + // + // Returns < 0 if the parse fails. + // Returns 0 if more data is needed. + // Returning > 0 indicates success & the number of bytes parsed. + int Parse(const uint8* buf, int size); + + // Returns true if the entire list has been parsed. + bool IsParsingComplete() const; + + private: + enum State { + NEED_LIST_HEADER, + INSIDE_LIST, + DONE_PARSING_LIST, + PARSE_ERROR, + }; + + struct ListState { + int id_; + int64 size_; + int64 bytes_parsed_; + const ListElementInfo* element_info_; + WebMParserClient* client_; + }; + + void ChangeState(State new_state); + + // Parses a single element in the current list. + // + // |header_size| - The size of the element header + // |id| - The ID of the element being parsed. + // |element_size| - The size of the element body. + // |data| - Pointer to the element contents. + // |size| - Number of bytes in |data| + // |client| - Client to pass the parsed data to. + // + // Returns < 0 if the parse fails. + // Returns 0 if more data is needed. + // Returning > 0 indicates success & the number of bytes parsed. + int ParseListElement(int header_size, + int id, int64 element_size, + const uint8* data, int size); + + // Called when starting to parse a new list. + // + // |id| - The ID of the new list. + // |size| - The size of the new list. + // |client| - The client object to notify that a new list is being parsed. + // + // Returns true if this list can be started in the current context. False + // if starting this list causes some sort of parse error. + bool OnListStart(int id, int64 size); + + // Called when the end of the current list has been reached. This may also + // signal the end of the current list's ancestors if the current list happens + // to be at the end of its parent. + // + // Returns true if no errors occurred while ending this list(s). + bool OnListEnd(); + + // Checks to see if |id_b| is a sibling or ancestor of |id_a|. + bool IsSiblingOrAncestor(int id_a, int id_b) const; + + State state_; + + // Element ID passed to the constructor. + const int root_id_; + + // Element level for |root_id_|. Used to verify that elements appear at + // the correct level. + const int root_level_; + + // WebMParserClient to handle the root list. + WebMParserClient* const root_client_; + + // Stack of state for all the lists currently being parsed. Lists are + // added and removed from this stack as they are parsed. + std::vector list_state_stack_; + + DISALLOW_COPY_AND_ASSIGN(WebMListParser); +}; + +// Parses an element header & returns the ID and element size. +// +// Returns < 0 if the parse fails. +// Returns 0 if more data is needed. +// Returning > 0 indicates success & the number of bytes parsed. +// |*id| contains the element ID on success and is undefined otherwise. +// |*element_size| contains the element size on success and is undefined +// otherwise. +int MEDIA_EXPORT WebMParseElementHeader(const uint8* buf, int size, + int* id, int64* element_size); + +} // namespace media + +#endif // MEDIA_FORMATS_WEBM_WEBM_PARSER_H_ diff --git a/packager/media/formats/webm/webm_parser_unittest.cc b/packager/media/formats/webm/webm_parser_unittest.cc new file mode 100644 index 0000000000..a1249e89c4 --- /dev/null +++ b/packager/media/formats/webm/webm_parser_unittest.cc @@ -0,0 +1,412 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "media/formats/webm/cluster_builder.h" +#include "media/formats/webm/webm_constants.h" +#include "media/formats/webm/webm_parser.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using ::testing::InSequence; +using ::testing::Return; +using ::testing::ReturnNull; +using ::testing::StrictMock; +using ::testing::_; + +namespace media { + +enum { kBlockCount = 5 }; + +class MockWebMParserClient : public WebMParserClient { + public: + virtual ~MockWebMParserClient() {} + + // WebMParserClient methods. + MOCK_METHOD1(OnListStart, WebMParserClient*(int)); + MOCK_METHOD1(OnListEnd, bool(int)); + MOCK_METHOD2(OnUInt, bool(int, int64)); + MOCK_METHOD2(OnFloat, bool(int, double)); + MOCK_METHOD3(OnBinary, bool(int, const uint8*, int)); + MOCK_METHOD2(OnString, bool(int, const std::string&)); +}; + +class WebMParserTest : public testing::Test { + protected: + StrictMock client_; +}; + +static scoped_ptr CreateCluster(int block_count) { + ClusterBuilder cb; + cb.SetClusterTimecode(0); + + for (int i = 0; i < block_count; i++) { + uint8 data[] = { 0x00 }; + cb.AddSimpleBlock(0, i, 0, data, sizeof(data)); + } + + return cb.Finish(); +} + +static void CreateClusterExpectations(int block_count, + bool is_complete_cluster, + MockWebMParserClient* client) { + + InSequence s; + EXPECT_CALL(*client, OnListStart(kWebMIdCluster)).WillOnce(Return(client)); + EXPECT_CALL(*client, OnUInt(kWebMIdTimecode, 0)) + .WillOnce(Return(true)); + + for (int i = 0; i < block_count; i++) { + EXPECT_CALL(*client, OnBinary(kWebMIdSimpleBlock, _, _)) + .WillOnce(Return(true)); + } + + if (is_complete_cluster) + EXPECT_CALL(*client, OnListEnd(kWebMIdCluster)).WillOnce(Return(true)); +} + +TEST_F(WebMParserTest, EmptyCluster) { + const uint8 kEmptyCluster[] = { + 0x1F, 0x43, 0xB6, 0x75, 0x80 // CLUSTER (size = 0) + }; + int size = sizeof(kEmptyCluster); + + InSequence s; + EXPECT_CALL(client_, OnListStart(kWebMIdCluster)).WillOnce(Return(&client_)); + EXPECT_CALL(client_, OnListEnd(kWebMIdCluster)).WillOnce(Return(true)); + + WebMListParser parser(kWebMIdCluster, &client_); + EXPECT_EQ(size, parser.Parse(kEmptyCluster, size)); + EXPECT_TRUE(parser.IsParsingComplete()); +} + +TEST_F(WebMParserTest, EmptyClusterInSegment) { + const uint8 kBuffer[] = { + 0x18, 0x53, 0x80, 0x67, 0x85, // SEGMENT (size = 5) + 0x1F, 0x43, 0xB6, 0x75, 0x80, // CLUSTER (size = 0) + }; + int size = sizeof(kBuffer); + + InSequence s; + EXPECT_CALL(client_, OnListStart(kWebMIdSegment)).WillOnce(Return(&client_)); + EXPECT_CALL(client_, OnListStart(kWebMIdCluster)).WillOnce(Return(&client_)); + EXPECT_CALL(client_, OnListEnd(kWebMIdCluster)).WillOnce(Return(true)); + EXPECT_CALL(client_, OnListEnd(kWebMIdSegment)).WillOnce(Return(true)); + + WebMListParser parser(kWebMIdSegment, &client_); + EXPECT_EQ(size, parser.Parse(kBuffer, size)); + EXPECT_TRUE(parser.IsParsingComplete()); +} + +// Test the case where a non-list child element has a size +// that is beyond the end of the parent. +TEST_F(WebMParserTest, ChildNonListLargerThanParent) { + const uint8 kBuffer[] = { + 0x1F, 0x43, 0xB6, 0x75, 0x81, // CLUSTER (size = 1) + 0xE7, 0x81, 0x01, // Timecode (size=1, value=1) + }; + + InSequence s; + EXPECT_CALL(client_, OnListStart(kWebMIdCluster)).WillOnce(Return(&client_)); + + WebMListParser parser(kWebMIdCluster, &client_); + EXPECT_EQ(-1, parser.Parse(kBuffer, sizeof(kBuffer))); + EXPECT_FALSE(parser.IsParsingComplete()); +} + +// Test the case where a list child element has a size +// that is beyond the end of the parent. +TEST_F(WebMParserTest, ChildListLargerThanParent) { + const uint8 kBuffer[] = { + 0x18, 0x53, 0x80, 0x67, 0x85, // SEGMENT (size = 5) + 0x1F, 0x43, 0xB6, 0x75, 0x81, 0x11 // CLUSTER (size = 1) + }; + + InSequence s; + EXPECT_CALL(client_, OnListStart(kWebMIdSegment)).WillOnce(Return(&client_)); + + WebMListParser parser(kWebMIdSegment, &client_); + EXPECT_EQ(-1, parser.Parse(kBuffer, sizeof(kBuffer))); + EXPECT_FALSE(parser.IsParsingComplete()); +} + +// Expecting to parse a Cluster, but get a Segment. +TEST_F(WebMParserTest, ListIdDoesNotMatch) { + const uint8 kBuffer[] = { + 0x18, 0x53, 0x80, 0x67, 0x80, // SEGMENT (size = 0) + }; + + WebMListParser parser(kWebMIdCluster, &client_); + EXPECT_EQ(-1, parser.Parse(kBuffer, sizeof(kBuffer))); + EXPECT_FALSE(parser.IsParsingComplete()); +} + +TEST_F(WebMParserTest, InvalidElementInList) { + const uint8 kBuffer[] = { + 0x18, 0x53, 0x80, 0x67, 0x82, // SEGMENT (size = 2) + 0xAE, 0x80, // TrackEntry (size = 0) + }; + + InSequence s; + EXPECT_CALL(client_, OnListStart(kWebMIdSegment)).WillOnce(Return(&client_)); + + WebMListParser parser(kWebMIdSegment, &client_); + EXPECT_EQ(-1, parser.Parse(kBuffer, sizeof(kBuffer))); + EXPECT_FALSE(parser.IsParsingComplete()); +} + +// Test specific case of InvalidElementInList to verify EBMLHEADER within +// known-sized cluster causes parse error. +TEST_F(WebMParserTest, InvalidEBMLHeaderInCluster) { + const uint8 kBuffer[] = { + 0x1F, 0x43, 0xB6, 0x75, 0x85, // CLUSTER (size = 5) + 0x1A, 0x45, 0xDF, 0xA3, 0x80, // EBMLHEADER (size = 0) + }; + + InSequence s; + EXPECT_CALL(client_, OnListStart(kWebMIdCluster)).WillOnce(Return(&client_)); + + WebMListParser parser(kWebMIdCluster, &client_); + EXPECT_EQ(-1, parser.Parse(kBuffer, sizeof(kBuffer))); + EXPECT_FALSE(parser.IsParsingComplete()); +} + +// Verify that EBMLHEADER ends a preceding "unknown"-sized CLUSTER. +TEST_F(WebMParserTest, UnknownSizeClusterFollowedByEBMLHeader) { + const uint8 kBuffer[] = { + 0x1F, 0x43, 0xB6, 0x75, 0xFF, // CLUSTER (size = unknown; really 0 due to:) + 0x1A, 0x45, 0xDF, 0xA3, 0x80, // EBMLHEADER (size = 0) + }; + + InSequence s; + EXPECT_CALL(client_, OnListStart(kWebMIdCluster)).WillOnce(Return(&client_)); + EXPECT_CALL(client_, OnListEnd(kWebMIdCluster)).WillOnce(Return(true)); + + WebMListParser parser(kWebMIdCluster, &client_); + + // List parse should consume the CLUSTER but not the EBMLHEADER. + EXPECT_EQ(5, parser.Parse(kBuffer, sizeof(kBuffer))); + EXPECT_TRUE(parser.IsParsingComplete()); +} + +TEST_F(WebMParserTest, VoidAndCRC32InList) { + const uint8 kBuffer[] = { + 0x18, 0x53, 0x80, 0x67, 0x99, // SEGMENT (size = 25) + 0xEC, 0x83, 0x00, 0x00, 0x00, // Void (size = 3) + 0xBF, 0x83, 0x00, 0x00, 0x00, // CRC32 (size = 3) + 0x1F, 0x43, 0xB6, 0x75, 0x8A, // CLUSTER (size = 10) + 0xEC, 0x83, 0x00, 0x00, 0x00, // Void (size = 3) + 0xBF, 0x83, 0x00, 0x00, 0x00, // CRC32 (size = 3) + }; + int size = sizeof(kBuffer); + + InSequence s; + EXPECT_CALL(client_, OnListStart(kWebMIdSegment)).WillOnce(Return(&client_)); + EXPECT_CALL(client_, OnListStart(kWebMIdCluster)).WillOnce(Return(&client_)); + EXPECT_CALL(client_, OnListEnd(kWebMIdCluster)).WillOnce(Return(true)); + EXPECT_CALL(client_, OnListEnd(kWebMIdSegment)).WillOnce(Return(true)); + + WebMListParser parser(kWebMIdSegment, &client_); + EXPECT_EQ(size, parser.Parse(kBuffer, size)); + EXPECT_TRUE(parser.IsParsingComplete()); +} + + +TEST_F(WebMParserTest, ParseListElementWithSingleCall) { + scoped_ptr cluster(CreateCluster(kBlockCount)); + CreateClusterExpectations(kBlockCount, true, &client_); + + WebMListParser parser(kWebMIdCluster, &client_); + EXPECT_EQ(cluster->size(), parser.Parse(cluster->data(), cluster->size())); + EXPECT_TRUE(parser.IsParsingComplete()); +} + +TEST_F(WebMParserTest, ParseListElementWithMultipleCalls) { + scoped_ptr cluster(CreateCluster(kBlockCount)); + CreateClusterExpectations(kBlockCount, true, &client_); + + const uint8* data = cluster->data(); + int size = cluster->size(); + int default_parse_size = 3; + WebMListParser parser(kWebMIdCluster, &client_); + int parse_size = std::min(default_parse_size, size); + + while (size > 0) { + int result = parser.Parse(data, parse_size); + ASSERT_GE(result, 0); + ASSERT_LE(result, parse_size); + + if (result == 0) { + // The parser needs more data so increase the parse_size a little. + EXPECT_FALSE(parser.IsParsingComplete()); + parse_size += default_parse_size; + parse_size = std::min(parse_size, size); + continue; + } + + parse_size = default_parse_size; + + data += result; + size -= result; + + EXPECT_EQ((size == 0), parser.IsParsingComplete()); + } + EXPECT_TRUE(parser.IsParsingComplete()); +} + +TEST_F(WebMParserTest, Reset) { + InSequence s; + scoped_ptr cluster(CreateCluster(kBlockCount)); + + // First expect all but the last block. + CreateClusterExpectations(kBlockCount - 1, false, &client_); + + // Now expect all blocks. + CreateClusterExpectations(kBlockCount, true, &client_); + + WebMListParser parser(kWebMIdCluster, &client_); + + // Send slightly less than the full cluster so all but the last block is + // parsed. + int result = parser.Parse(cluster->data(), cluster->size() - 1); + EXPECT_GT(result, 0); + EXPECT_LT(result, cluster->size()); + EXPECT_FALSE(parser.IsParsingComplete()); + + parser.Reset(); + + // Now parse a whole cluster to verify that all the blocks will get parsed. + EXPECT_EQ(cluster->size(), parser.Parse(cluster->data(), cluster->size())); + EXPECT_TRUE(parser.IsParsingComplete()); +} + +// Test the case where multiple clients are used for different lists. +TEST_F(WebMParserTest, MultipleClients) { + const uint8 kBuffer[] = { + 0x18, 0x53, 0x80, 0x67, 0x94, // SEGMENT (size = 20) + 0x16, 0x54, 0xAE, 0x6B, 0x85, // TRACKS (size = 5) + 0xAE, 0x83, // TRACKENTRY (size = 3) + 0xD7, 0x81, 0x01, // TRACKNUMBER (size = 1) + 0x1F, 0x43, 0xB6, 0x75, 0x85, // CLUSTER (size = 5) + 0xEC, 0x83, 0x00, 0x00, 0x00, // Void (size = 3) + }; + int size = sizeof(kBuffer); + + StrictMock c1_; + StrictMock c2_; + StrictMock c3_; + + InSequence s; + EXPECT_CALL(client_, OnListStart(kWebMIdSegment)).WillOnce(Return(&c1_)); + EXPECT_CALL(c1_, OnListStart(kWebMIdTracks)).WillOnce(Return(&c2_)); + EXPECT_CALL(c2_, OnListStart(kWebMIdTrackEntry)).WillOnce(Return(&c3_)); + EXPECT_CALL(c3_, OnUInt(kWebMIdTrackNumber, 1)).WillOnce(Return(true)); + EXPECT_CALL(c2_, OnListEnd(kWebMIdTrackEntry)).WillOnce(Return(true)); + EXPECT_CALL(c1_, OnListEnd(kWebMIdTracks)).WillOnce(Return(true)); + EXPECT_CALL(c1_, OnListStart(kWebMIdCluster)).WillOnce(Return(&c2_)); + EXPECT_CALL(c1_, OnListEnd(kWebMIdCluster)).WillOnce(Return(true)); + EXPECT_CALL(client_, OnListEnd(kWebMIdSegment)).WillOnce(Return(true)); + + WebMListParser parser(kWebMIdSegment, &client_); + EXPECT_EQ(size, parser.Parse(kBuffer, size)); + EXPECT_TRUE(parser.IsParsingComplete()); +} + +// Test the case where multiple clients are used for different lists. +TEST_F(WebMParserTest, InvalidClient) { + const uint8 kBuffer[] = { + 0x18, 0x53, 0x80, 0x67, 0x85, // SEGMENT (size = 20) + 0x16, 0x54, 0xAE, 0x6B, 0x80, // TRACKS (size = 5) + }; + + InSequence s; + EXPECT_CALL(client_, OnListStart(kWebMIdSegment)).WillOnce(ReturnNull()); + + WebMListParser parser(kWebMIdSegment, &client_); + EXPECT_EQ(-1, parser.Parse(kBuffer, sizeof(kBuffer))); + EXPECT_FALSE(parser.IsParsingComplete()); +} + +TEST_F(WebMParserTest, ReservedIds) { + const uint8 k1ByteReservedId[] = { 0xFF, 0x81 }; + const uint8 k2ByteReservedId[] = { 0x7F, 0xFF, 0x81 }; + const uint8 k3ByteReservedId[] = { 0x3F, 0xFF, 0xFF, 0x81 }; + const uint8 k4ByteReservedId[] = { 0x1F, 0xFF, 0xFF, 0xFF, 0x81 }; + const uint8* kBuffers[] = { + k1ByteReservedId, + k2ByteReservedId, + k3ByteReservedId, + k4ByteReservedId + }; + + for (size_t i = 0; i < arraysize(kBuffers); i++) { + int id; + int64 element_size; + int buffer_size = 2 + i; + EXPECT_EQ(buffer_size, WebMParseElementHeader(kBuffers[i], buffer_size, + &id, &element_size)); + EXPECT_EQ(id, kWebMReservedId); + EXPECT_EQ(element_size, 1); + } +} + +TEST_F(WebMParserTest, ReservedSizes) { + const uint8 k1ByteReservedSize[] = { 0xA3, 0xFF }; + const uint8 k2ByteReservedSize[] = { 0xA3, 0x7F, 0xFF }; + const uint8 k3ByteReservedSize[] = { 0xA3, 0x3F, 0xFF, 0xFF }; + const uint8 k4ByteReservedSize[] = { 0xA3, 0x1F, 0xFF, 0xFF, 0xFF }; + const uint8 k5ByteReservedSize[] = { 0xA3, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF }; + const uint8 k6ByteReservedSize[] = { 0xA3, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF }; + const uint8 k7ByteReservedSize[] = { 0xA3, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF }; + const uint8 k8ByteReservedSize[] = { 0xA3, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF }; + const uint8* kBuffers[] = { + k1ByteReservedSize, + k2ByteReservedSize, + k3ByteReservedSize, + k4ByteReservedSize, + k5ByteReservedSize, + k6ByteReservedSize, + k7ByteReservedSize, + k8ByteReservedSize + }; + + for (size_t i = 0; i < arraysize(kBuffers); i++) { + int id; + int64 element_size; + int buffer_size = 2 + i; + EXPECT_EQ(buffer_size, WebMParseElementHeader(kBuffers[i], buffer_size, + &id, &element_size)); + EXPECT_EQ(id, 0xA3); + EXPECT_EQ(element_size, kWebMUnknownSize); + } +} + +TEST_F(WebMParserTest, ZeroPaddedStrings) { + const uint8 kBuffer[] = { + 0x1A, 0x45, 0xDF, 0xA3, 0x91, // EBMLHEADER (size = 17) + 0x42, 0x82, 0x80, // DocType (size = 0) + 0x42, 0x82, 0x81, 0x00, // DocType (size = 1) "" + 0x42, 0x82, 0x81, 'a', // DocType (size = 1) "a" + 0x42, 0x82, 0x83, 'a', 0x00, 0x00 // DocType (size = 3) "a" + }; + int size = sizeof(kBuffer); + + InSequence s; + EXPECT_CALL(client_, OnListStart(kWebMIdEBMLHeader)) + .WillOnce(Return(&client_)); + EXPECT_CALL(client_, OnString(kWebMIdDocType, "")).WillOnce(Return(true)); + EXPECT_CALL(client_, OnString(kWebMIdDocType, "")).WillOnce(Return(true)); + EXPECT_CALL(client_, OnString(kWebMIdDocType, "a")).WillOnce(Return(true)); + EXPECT_CALL(client_, OnString(kWebMIdDocType, "a")).WillOnce(Return(true)); + EXPECT_CALL(client_, OnListEnd(kWebMIdEBMLHeader)).WillOnce(Return(true)); + + WebMListParser parser(kWebMIdEBMLHeader, &client_); + EXPECT_EQ(size, parser.Parse(kBuffer, size)); + EXPECT_TRUE(parser.IsParsingComplete()); +} + +} // namespace media diff --git a/packager/media/formats/webm/webm_stream_parser.cc b/packager/media/formats/webm/webm_stream_parser.cc new file mode 100644 index 0000000000..b978b96b4d --- /dev/null +++ b/packager/media/formats/webm/webm_stream_parser.cc @@ -0,0 +1,283 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "media/formats/webm/webm_stream_parser.h" + +#include + +#include "base/callback.h" +#include "base/callback_helpers.h" +#include "base/logging.h" +#include "media/base/timestamp_constants.h" +#include "media/formats/webm/webm_cluster_parser.h" +#include "media/formats/webm/webm_constants.h" +#include "media/formats/webm/webm_content_encodings.h" +#include "media/formats/webm/webm_info_parser.h" +#include "media/formats/webm/webm_tracks_parser.h" + +namespace media { + +WebMStreamParser::WebMStreamParser() + : state_(kWaitingForInit), + unknown_segment_size_(false) { +} + +WebMStreamParser::~WebMStreamParser() { +} + +void WebMStreamParser::Init( + const InitCB& init_cb, + const NewConfigCB& config_cb, + const NewBuffersCB& new_buffers_cb, + bool ignore_text_tracks, + const EncryptedMediaInitDataCB& encrypted_media_init_data_cb, + const NewMediaSegmentCB& new_segment_cb, + const base::Closure& end_of_segment_cb, + const scoped_refptr& media_log) { + DCHECK_EQ(state_, kWaitingForInit); + DCHECK(init_cb_.is_null()); + DCHECK(!init_cb.is_null()); + DCHECK(!config_cb.is_null()); + DCHECK(!new_buffers_cb.is_null()); + DCHECK(!encrypted_media_init_data_cb.is_null()); + DCHECK(!new_segment_cb.is_null()); + DCHECK(!end_of_segment_cb.is_null()); + + ChangeState(kParsingHeaders); + init_cb_ = init_cb; + config_cb_ = config_cb; + new_buffers_cb_ = new_buffers_cb; + ignore_text_tracks_ = ignore_text_tracks; + encrypted_media_init_data_cb_ = encrypted_media_init_data_cb; + new_segment_cb_ = new_segment_cb; + end_of_segment_cb_ = end_of_segment_cb; + media_log_ = media_log; +} + +void WebMStreamParser::Flush() { + DCHECK_NE(state_, kWaitingForInit); + + byte_queue_.Reset(); + if (cluster_parser_) + cluster_parser_->Reset(); + if (state_ == kParsingClusters) { + ChangeState(kParsingHeaders); + end_of_segment_cb_.Run(); + } +} + +bool WebMStreamParser::Parse(const uint8* buf, int size) { + DCHECK_NE(state_, kWaitingForInit); + + if (state_ == kError) + return false; + + byte_queue_.Push(buf, size); + + int result = 0; + int bytes_parsed = 0; + const uint8* cur = NULL; + int cur_size = 0; + + byte_queue_.Peek(&cur, &cur_size); + while (cur_size > 0) { + State oldState = state_; + switch (state_) { + case kParsingHeaders: + result = ParseInfoAndTracks(cur, cur_size); + break; + + case kParsingClusters: + result = ParseCluster(cur, cur_size); + break; + + case kWaitingForInit: + case kError: + return false; + } + + if (result < 0) { + ChangeState(kError); + return false; + } + + if (state_ == oldState && result == 0) + break; + + DCHECK_GE(result, 0); + cur += result; + cur_size -= result; + bytes_parsed += result; + } + + byte_queue_.Pop(bytes_parsed); + return true; +} + +void WebMStreamParser::ChangeState(State new_state) { + DVLOG(1) << "ChangeState() : " << state_ << " -> " << new_state; + state_ = new_state; +} + +int WebMStreamParser::ParseInfoAndTracks(const uint8* data, int size) { + DVLOG(2) << "ParseInfoAndTracks()"; + DCHECK(data); + DCHECK_GT(size, 0); + + const uint8* cur = data; + int cur_size = size; + int bytes_parsed = 0; + + int id; + int64 element_size; + int result = WebMParseElementHeader(cur, cur_size, &id, &element_size); + + if (result <= 0) + return result; + + switch (id) { + case kWebMIdEBMLHeader: + case kWebMIdSeekHead: + case kWebMIdVoid: + case kWebMIdCRC32: + case kWebMIdCues: + case kWebMIdChapters: + case kWebMIdTags: + case kWebMIdAttachments: + // TODO(matthewjheaney): Implement support for chapters. + if (cur_size < (result + element_size)) { + // We don't have the whole element yet. Signal we need more data. + return 0; + } + // Skip the element. + return result + element_size; + break; + case kWebMIdCluster: + if (!cluster_parser_) { + MEDIA_LOG(ERROR, media_log_) << "Found Cluster element before Info."; + return -1; + } + ChangeState(kParsingClusters); + new_segment_cb_.Run(); + return 0; + break; + case kWebMIdSegment: + // Segment of unknown size indicates live stream. + if (element_size == kWebMUnknownSize) + unknown_segment_size_ = true; + // Just consume the segment header. + return result; + break; + case kWebMIdInfo: + // We've found the element we are looking for. + break; + default: { + MEDIA_LOG(ERROR, media_log_) << "Unexpected element ID 0x" << std::hex + << id; + return -1; + } + } + + WebMInfoParser info_parser; + result = info_parser.Parse(cur, cur_size); + + if (result <= 0) + return result; + + cur += result; + cur_size -= result; + bytes_parsed += result; + + WebMTracksParser tracks_parser(media_log_, ignore_text_tracks_); + result = tracks_parser.Parse(cur, cur_size); + + if (result <= 0) + return result; + + bytes_parsed += result; + + double timecode_scale_in_us = info_parser.timecode_scale() / 1000.0; + InitParameters params(kInfiniteDuration()); + + if (info_parser.duration() > 0) { + int64 duration_in_us = info_parser.duration() * timecode_scale_in_us; + params.duration = base::TimeDelta::FromMicroseconds(duration_in_us); + } + + params.timeline_offset = info_parser.date_utc(); + + if (unknown_segment_size_ && (info_parser.duration() <= 0) && + !info_parser.date_utc().is_null()) { + params.liveness = DemuxerStream::LIVENESS_LIVE; + } else if (info_parser.duration() >= 0) { + params.liveness = DemuxerStream::LIVENESS_RECORDED; + } else { + params.liveness = DemuxerStream::LIVENESS_UNKNOWN; + } + + const AudioDecoderConfig& audio_config = tracks_parser.audio_decoder_config(); + if (audio_config.is_encrypted()) + OnEncryptedMediaInitData(tracks_parser.audio_encryption_key_id()); + + const VideoDecoderConfig& video_config = tracks_parser.video_decoder_config(); + if (video_config.is_encrypted()) + OnEncryptedMediaInitData(tracks_parser.video_encryption_key_id()); + + if (!config_cb_.Run(audio_config, + video_config, + tracks_parser.text_tracks())) { + DVLOG(1) << "New config data isn't allowed."; + return -1; + } + + cluster_parser_.reset(new WebMClusterParser( + info_parser.timecode_scale(), tracks_parser.audio_track_num(), + tracks_parser.GetAudioDefaultDuration(timecode_scale_in_us), + tracks_parser.video_track_num(), + tracks_parser.GetVideoDefaultDuration(timecode_scale_in_us), + tracks_parser.text_tracks(), tracks_parser.ignored_tracks(), + tracks_parser.audio_encryption_key_id(), + tracks_parser.video_encryption_key_id(), audio_config.codec(), + media_log_)); + + if (!init_cb_.is_null()) + base::ResetAndReturn(&init_cb_).Run(params); + + return bytes_parsed; +} + +int WebMStreamParser::ParseCluster(const uint8* data, int size) { + if (!cluster_parser_) + return -1; + + int bytes_parsed = cluster_parser_->Parse(data, size); + if (bytes_parsed < 0) + return bytes_parsed; + + const BufferQueue& audio_buffers = cluster_parser_->GetAudioBuffers(); + const BufferQueue& video_buffers = cluster_parser_->GetVideoBuffers(); + const TextBufferQueueMap& text_map = cluster_parser_->GetTextBuffers(); + + bool cluster_ended = cluster_parser_->cluster_ended(); + + if ((!audio_buffers.empty() || !video_buffers.empty() || + !text_map.empty()) && + !new_buffers_cb_.Run(audio_buffers, video_buffers, text_map)) { + return -1; + } + + if (cluster_ended) { + ChangeState(kParsingHeaders); + end_of_segment_cb_.Run(); + } + + return bytes_parsed; +} + +void WebMStreamParser::OnEncryptedMediaInitData(const std::string& key_id) { + std::vector key_id_vector(key_id.begin(), key_id.end()); + encrypted_media_init_data_cb_.Run(EmeInitDataType::WEBM, key_id_vector); +} + +} // namespace media diff --git a/packager/media/formats/webm/webm_stream_parser.h b/packager/media/formats/webm/webm_stream_parser.h new file mode 100644 index 0000000000..bb53c9440d --- /dev/null +++ b/packager/media/formats/webm/webm_stream_parser.h @@ -0,0 +1,89 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef MEDIA_FORMATS_WEBM_WEBM_STREAM_PARSER_H_ +#define MEDIA_FORMATS_WEBM_WEBM_STREAM_PARSER_H_ + +#include "base/callback_forward.h" +#include "base/memory/ref_counted.h" +#include "media/base/audio_decoder_config.h" +#include "media/base/byte_queue.h" +#include "media/base/stream_parser.h" +#include "media/base/video_decoder_config.h" + +namespace media { + +class WebMClusterParser; + +class WebMStreamParser : public StreamParser { + public: + WebMStreamParser(); + ~WebMStreamParser() override; + + // StreamParser implementation. + void Init(const InitCB& init_cb, + const NewConfigCB& config_cb, + const NewBuffersCB& new_buffers_cb, + bool ignore_text_tracks, + const EncryptedMediaInitDataCB& encrypted_media_init_data_cb, + const NewMediaSegmentCB& new_segment_cb, + const base::Closure& end_of_segment_cb, + const scoped_refptr& media_log) override; + void Flush() override; + bool Parse(const uint8* buf, int size) override; + + private: + enum State { + kWaitingForInit, + kParsingHeaders, + kParsingClusters, + kError + }; + + void ChangeState(State new_state); + + // Parses WebM Header, Info, Tracks elements. It also skips other level 1 + // elements that are not used right now. Once the Info & Tracks elements have + // been parsed, this method will transition the parser from PARSING_HEADERS to + // PARSING_CLUSTERS. + // + // Returns < 0 if the parse fails. + // Returns 0 if more data is needed. + // Returning > 0 indicates success & the number of bytes parsed. + int ParseInfoAndTracks(const uint8* data, int size); + + // Incrementally parses WebM cluster elements. This method also skips + // CUES elements if they are encountered since we currently don't use the + // data in these elements. + // + // Returns < 0 if the parse fails. + // Returns 0 if more data is needed. + // Returning > 0 indicates success & the number of bytes parsed. + int ParseCluster(const uint8* data, int size); + + // Fire needkey event through the |encrypted_media_init_data_cb_|. + void OnEncryptedMediaInitData(const std::string& key_id); + + State state_; + InitCB init_cb_; + NewConfigCB config_cb_; + NewBuffersCB new_buffers_cb_; + bool ignore_text_tracks_; + EncryptedMediaInitDataCB encrypted_media_init_data_cb_; + + NewMediaSegmentCB new_segment_cb_; + base::Closure end_of_segment_cb_; + scoped_refptr media_log_; + + bool unknown_segment_size_; + + scoped_ptr cluster_parser_; + ByteQueue byte_queue_; + + DISALLOW_COPY_AND_ASSIGN(WebMStreamParser); +}; + +} // namespace media + +#endif // MEDIA_FORMATS_WEBM_WEBM_STREAM_PARSER_H_ diff --git a/packager/media/formats/webm/webm_tracks_parser.cc b/packager/media/formats/webm/webm_tracks_parser.cc new file mode 100644 index 0000000000..1f2ca69afd --- /dev/null +++ b/packager/media/formats/webm/webm_tracks_parser.cc @@ -0,0 +1,344 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "media/formats/webm/webm_tracks_parser.h" + +#include "base/logging.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "media/base/timestamp_constants.h" +#include "media/formats/webm/webm_constants.h" +#include "media/formats/webm/webm_content_encodings.h" + +namespace media { + +static TextKind CodecIdToTextKind(const std::string& codec_id) { + if (codec_id == kWebMCodecSubtitles) + return kTextSubtitles; + + if (codec_id == kWebMCodecCaptions) + return kTextCaptions; + + if (codec_id == kWebMCodecDescriptions) + return kTextDescriptions; + + if (codec_id == kWebMCodecMetadata) + return kTextMetadata; + + return kTextNone; +} + +static base::TimeDelta PrecisionCappedDefaultDuration( + const double timecode_scale_in_us, const int64 duration_in_ns) { + if (duration_in_ns <= 0) + return kNoTimestamp(); + + int64 mult = duration_in_ns / 1000; + mult /= timecode_scale_in_us; + if (mult == 0) + return kNoTimestamp(); + + mult = static_cast(mult) * timecode_scale_in_us; + return base::TimeDelta::FromMicroseconds(mult); +} + +WebMTracksParser::WebMTracksParser(const scoped_refptr& media_log, + bool ignore_text_tracks) + : track_type_(-1), + track_num_(-1), + seek_preroll_(-1), + codec_delay_(-1), + default_duration_(-1), + audio_track_num_(-1), + audio_default_duration_(-1), + video_track_num_(-1), + video_default_duration_(-1), + ignore_text_tracks_(ignore_text_tracks), + media_log_(media_log), + audio_client_(media_log), + video_client_(media_log) { +} + +WebMTracksParser::~WebMTracksParser() {} + +int WebMTracksParser::Parse(const uint8* buf, int size) { + track_type_ =-1; + track_num_ = -1; + default_duration_ = -1; + track_name_.clear(); + track_language_.clear(); + audio_track_num_ = -1; + audio_default_duration_ = -1; + audio_decoder_config_ = AudioDecoderConfig(); + video_track_num_ = -1; + video_default_duration_ = -1; + video_decoder_config_ = VideoDecoderConfig(); + text_tracks_.clear(); + ignored_tracks_.clear(); + + WebMListParser parser(kWebMIdTracks, this); + int result = parser.Parse(buf, size); + + if (result <= 0) + return result; + + // For now we do all or nothing parsing. + return parser.IsParsingComplete() ? result : 0; +} + +base::TimeDelta WebMTracksParser::GetAudioDefaultDuration( + const double timecode_scale_in_us) const { + return PrecisionCappedDefaultDuration(timecode_scale_in_us, + audio_default_duration_); +} + +base::TimeDelta WebMTracksParser::GetVideoDefaultDuration( + const double timecode_scale_in_us) const { + return PrecisionCappedDefaultDuration(timecode_scale_in_us, + video_default_duration_); +} + +WebMParserClient* WebMTracksParser::OnListStart(int id) { + if (id == kWebMIdContentEncodings) { + DCHECK(!track_content_encodings_client_.get()); + track_content_encodings_client_.reset( + new WebMContentEncodingsClient(media_log_)); + return track_content_encodings_client_->OnListStart(id); + } + + if (id == kWebMIdTrackEntry) { + track_type_ = -1; + track_num_ = -1; + default_duration_ = -1; + track_name_.clear(); + track_language_.clear(); + codec_id_ = ""; + codec_private_.clear(); + audio_client_.Reset(); + video_client_.Reset(); + return this; + } + + if (id == kWebMIdAudio) + return &audio_client_; + + if (id == kWebMIdVideo) + return &video_client_; + + return this; +} + +bool WebMTracksParser::OnListEnd(int id) { + if (id == kWebMIdContentEncodings) { + DCHECK(track_content_encodings_client_.get()); + return track_content_encodings_client_->OnListEnd(id); + } + + if (id == kWebMIdTrackEntry) { + if (track_type_ == -1 || track_num_ == -1) { + MEDIA_LOG(ERROR, media_log_) << "Missing TrackEntry data for " + << " TrackType " << track_type_ + << " TrackNum " << track_num_; + return false; + } + + if (track_type_ != kWebMTrackTypeAudio && + track_type_ != kWebMTrackTypeVideo && + track_type_ != kWebMTrackTypeSubtitlesOrCaptions && + track_type_ != kWebMTrackTypeDescriptionsOrMetadata) { + MEDIA_LOG(ERROR, media_log_) << "Unexpected TrackType " << track_type_; + return false; + } + + TextKind text_track_kind = kTextNone; + if (track_type_ == kWebMTrackTypeSubtitlesOrCaptions) { + text_track_kind = CodecIdToTextKind(codec_id_); + if (text_track_kind == kTextNone) { + MEDIA_LOG(ERROR, media_log_) << "Missing TrackEntry CodecID" + << " TrackNum " << track_num_; + return false; + } + + if (text_track_kind != kTextSubtitles && + text_track_kind != kTextCaptions) { + MEDIA_LOG(ERROR, media_log_) << "Wrong TrackEntry CodecID" + << " TrackNum " << track_num_; + return false; + } + } else if (track_type_ == kWebMTrackTypeDescriptionsOrMetadata) { + text_track_kind = CodecIdToTextKind(codec_id_); + if (text_track_kind == kTextNone) { + MEDIA_LOG(ERROR, media_log_) << "Missing TrackEntry CodecID" + << " TrackNum " << track_num_; + return false; + } + + if (text_track_kind != kTextDescriptions && + text_track_kind != kTextMetadata) { + MEDIA_LOG(ERROR, media_log_) << "Wrong TrackEntry CodecID" + << " TrackNum " << track_num_; + return false; + } + } + + std::string encryption_key_id; + if (track_content_encodings_client_) { + DCHECK(!track_content_encodings_client_->content_encodings().empty()); + // If we have multiple ContentEncoding in one track. Always choose the + // key id in the first ContentEncoding as the key id of the track. + encryption_key_id = track_content_encodings_client_-> + content_encodings()[0]->encryption_key_id(); + } + + if (track_type_ == kWebMTrackTypeAudio) { + if (audio_track_num_ == -1) { + audio_track_num_ = track_num_; + audio_encryption_key_id_ = encryption_key_id; + + if (default_duration_ == 0) { + MEDIA_LOG(ERROR, media_log_) << "Illegal 0ns audio TrackEntry " + "DefaultDuration"; + return false; + } + audio_default_duration_ = default_duration_; + + DCHECK(!audio_decoder_config_.IsValidConfig()); + if (!audio_client_.InitializeConfig( + codec_id_, codec_private_, seek_preroll_, codec_delay_, + !audio_encryption_key_id_.empty(), &audio_decoder_config_)) { + return false; + } + } else { + MEDIA_LOG(DEBUG, media_log_) << "Ignoring audio track " << track_num_; + ignored_tracks_.insert(track_num_); + } + } else if (track_type_ == kWebMTrackTypeVideo) { + if (video_track_num_ == -1) { + video_track_num_ = track_num_; + video_encryption_key_id_ = encryption_key_id; + + if (default_duration_ == 0) { + MEDIA_LOG(ERROR, media_log_) << "Illegal 0ns video TrackEntry " + "DefaultDuration"; + return false; + } + video_default_duration_ = default_duration_; + + DCHECK(!video_decoder_config_.IsValidConfig()); + if (!video_client_.InitializeConfig( + codec_id_, codec_private_, !video_encryption_key_id_.empty(), + &video_decoder_config_)) { + return false; + } + } else { + MEDIA_LOG(DEBUG, media_log_) << "Ignoring video track " << track_num_; + ignored_tracks_.insert(track_num_); + } + } else if (track_type_ == kWebMTrackTypeSubtitlesOrCaptions || + track_type_ == kWebMTrackTypeDescriptionsOrMetadata) { + if (ignore_text_tracks_) { + MEDIA_LOG(DEBUG, media_log_) << "Ignoring text track " << track_num_; + ignored_tracks_.insert(track_num_); + } else { + std::string track_num = base::Int64ToString(track_num_); + text_tracks_[track_num_] = TextTrackConfig( + text_track_kind, track_name_, track_language_, track_num); + } + } else { + MEDIA_LOG(ERROR, media_log_) << "Unexpected TrackType " << track_type_; + return false; + } + + track_type_ = -1; + track_num_ = -1; + default_duration_ = -1; + track_name_.clear(); + track_language_.clear(); + codec_id_ = ""; + codec_private_.clear(); + track_content_encodings_client_.reset(); + + audio_client_.Reset(); + video_client_.Reset(); + return true; + } + + return true; +} + +bool WebMTracksParser::OnUInt(int id, int64 val) { + int64* dst = NULL; + + switch (id) { + case kWebMIdTrackNumber: + dst = &track_num_; + break; + case kWebMIdTrackType: + dst = &track_type_; + break; + case kWebMIdSeekPreRoll: + dst = &seek_preroll_; + break; + case kWebMIdCodecDelay: + dst = &codec_delay_; + break; + case kWebMIdDefaultDuration: + dst = &default_duration_; + break; + default: + return true; + } + + if (*dst != -1) { + MEDIA_LOG(ERROR, media_log_) << "Multiple values for id " << std::hex << id + << " specified"; + return false; + } + + *dst = val; + return true; +} + +bool WebMTracksParser::OnFloat(int id, double val) { + return true; +} + +bool WebMTracksParser::OnBinary(int id, const uint8* data, int size) { + if (id == kWebMIdCodecPrivate) { + if (!codec_private_.empty()) { + MEDIA_LOG(ERROR, media_log_) + << "Multiple CodecPrivate fields in a track."; + return false; + } + codec_private_.assign(data, data + size); + return true; + } + return true; +} + +bool WebMTracksParser::OnString(int id, const std::string& str) { + if (id == kWebMIdCodecID) { + if (!codec_id_.empty()) { + MEDIA_LOG(ERROR, media_log_) << "Multiple CodecID fields in a track"; + return false; + } + + codec_id_ = str; + return true; + } + + if (id == kWebMIdName) { + track_name_ = str; + return true; + } + + if (id == kWebMIdLanguage) { + track_language_ = str; + return true; + } + + return true; +} + +} // namespace media diff --git a/packager/media/formats/webm/webm_tracks_parser.h b/packager/media/formats/webm/webm_tracks_parser.h new file mode 100644 index 0000000000..d5513b520d --- /dev/null +++ b/packager/media/formats/webm/webm_tracks_parser.h @@ -0,0 +1,119 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef MEDIA_FORMATS_WEBM_WEBM_TRACKS_PARSER_H_ +#define MEDIA_FORMATS_WEBM_WEBM_TRACKS_PARSER_H_ + +#include +#include +#include +#include + +#include "base/compiler_specific.h" +#include "base/memory/scoped_ptr.h" +#include "base/time/time.h" +#include "media/base/audio_decoder_config.h" +#include "media/base/media_log.h" +#include "media/base/text_track_config.h" +#include "media/base/video_decoder_config.h" +#include "media/formats/webm/webm_audio_client.h" +#include "media/formats/webm/webm_content_encodings_client.h" +#include "media/formats/webm/webm_parser.h" +#include "media/formats/webm/webm_video_client.h" + +namespace media { + +// Parser for WebM Tracks element. +class MEDIA_EXPORT WebMTracksParser : public WebMParserClient { + public: + WebMTracksParser(const scoped_refptr& media_log, + bool ignore_text_tracks); + ~WebMTracksParser() override; + + // Parses a WebM Tracks element in |buf|. + // + // Returns -1 if the parse fails. + // Returns 0 if more data is needed. + // Returns the number of bytes parsed on success. + int Parse(const uint8* buf, int size); + + int64 audio_track_num() const { return audio_track_num_; } + int64 video_track_num() const { return video_track_num_; } + + // If TrackEntry DefaultDuration field existed for the associated audio or + // video track, returns that value converted from ns to base::TimeDelta with + // precision not greater than |timecode_scale_in_us|. Defaults to + // kNoTimestamp(). + base::TimeDelta GetAudioDefaultDuration( + const double timecode_scale_in_us) const; + base::TimeDelta GetVideoDefaultDuration( + const double timecode_scale_in_us) const; + + const std::set& ignored_tracks() const { return ignored_tracks_; } + + const std::string& audio_encryption_key_id() const { + return audio_encryption_key_id_; + } + + const AudioDecoderConfig& audio_decoder_config() { + return audio_decoder_config_; + } + + const std::string& video_encryption_key_id() const { + return video_encryption_key_id_; + } + + const VideoDecoderConfig& video_decoder_config() { + return video_decoder_config_; + } + + typedef std::map TextTracks; + + const TextTracks& text_tracks() const { + return text_tracks_; + } + + private: + // WebMParserClient implementation. + WebMParserClient* OnListStart(int id) override; + bool OnListEnd(int id) override; + bool OnUInt(int id, int64 val) override; + bool OnFloat(int id, double val) override; + bool OnBinary(int id, const uint8* data, int size) override; + bool OnString(int id, const std::string& str) override; + + int64 track_type_; + int64 track_num_; + std::string track_name_; + std::string track_language_; + std::string codec_id_; + std::vector codec_private_; + int64 seek_preroll_; + int64 codec_delay_; + int64 default_duration_; + scoped_ptr track_content_encodings_client_; + + int64 audio_track_num_; + int64 audio_default_duration_; + int64 video_track_num_; + int64 video_default_duration_; + bool ignore_text_tracks_; + TextTracks text_tracks_; + std::set ignored_tracks_; + std::string audio_encryption_key_id_; + std::string video_encryption_key_id_; + scoped_refptr media_log_; + + WebMAudioClient audio_client_; + AudioDecoderConfig audio_decoder_config_; + + WebMVideoClient video_client_; + VideoDecoderConfig video_decoder_config_; + + DISALLOW_COPY_AND_ASSIGN(WebMTracksParser); +}; + +} // namespace media + +#endif // MEDIA_FORMATS_WEBM_WEBM_TRACKS_PARSER_H_ diff --git a/packager/media/formats/webm/webm_tracks_parser_unittest.cc b/packager/media/formats/webm/webm_tracks_parser_unittest.cc new file mode 100644 index 0000000000..9c424ec3af --- /dev/null +++ b/packager/media/formats/webm/webm_tracks_parser_unittest.cc @@ -0,0 +1,211 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/logging.h" +#include "media/base/channel_layout.h" +#include "media/base/mock_media_log.h" +#include "media/base/timestamp_constants.h" +#include "media/formats/webm/tracks_builder.h" +#include "media/formats/webm/webm_constants.h" +#include "media/formats/webm/webm_tracks_parser.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using ::testing::HasSubstr; +using ::testing::InSequence; +using ::testing::Return; +using ::testing::StrictMock; +using ::testing::_; + +namespace media { + +static const double kDefaultTimecodeScaleInUs = 1000.0; // 1 ms resolution + +class WebMTracksParserTest : public testing::Test { + public: + WebMTracksParserTest() : media_log_(new StrictMock()) {} + + protected: + void VerifyTextTrackInfo(const uint8* buffer, + int buffer_size, + TextKind text_kind, + const std::string& name, + const std::string& language) { + scoped_ptr parser( + new WebMTracksParser(media_log_, false)); + + int result = parser->Parse(buffer, buffer_size); + EXPECT_GT(result, 0); + EXPECT_EQ(result, buffer_size); + + const WebMTracksParser::TextTracks& text_tracks = parser->text_tracks(); + EXPECT_EQ(text_tracks.size(), WebMTracksParser::TextTracks::size_type(1)); + + const WebMTracksParser::TextTracks::const_iterator itr = + text_tracks.begin(); + EXPECT_EQ(itr->first, 1); // track num + + const TextTrackConfig& config = itr->second; + EXPECT_EQ(config.kind(), text_kind); + EXPECT_TRUE(config.label() == name); + EXPECT_TRUE(config.language() == language); + } + + scoped_refptr> media_log_; +}; + +TEST_F(WebMTracksParserTest, SubtitleNoNameNoLang) { + InSequence s; + + TracksBuilder tb; + tb.AddTextTrack(1, 1, kWebMCodecSubtitles, "", ""); + + const std::vector buf = tb.Finish(); + VerifyTextTrackInfo(&buf[0], buf.size(), kTextSubtitles, "", ""); +} + +TEST_F(WebMTracksParserTest, SubtitleYesNameNoLang) { + InSequence s; + + TracksBuilder tb; + tb.AddTextTrack(1, 1, kWebMCodecSubtitles, "Spock", ""); + + const std::vector buf = tb.Finish(); + VerifyTextTrackInfo(&buf[0], buf.size(), kTextSubtitles, "Spock", ""); +} + +TEST_F(WebMTracksParserTest, SubtitleNoNameYesLang) { + InSequence s; + + TracksBuilder tb; + tb.AddTextTrack(1, 1, kWebMCodecSubtitles, "", "eng"); + + const std::vector buf = tb.Finish(); + VerifyTextTrackInfo(&buf[0], buf.size(), kTextSubtitles, "", "eng"); +} + +TEST_F(WebMTracksParserTest, SubtitleYesNameYesLang) { + InSequence s; + + TracksBuilder tb; + tb.AddTextTrack(1, 1, kWebMCodecSubtitles, "Picard", "fre"); + + const std::vector buf = tb.Finish(); + VerifyTextTrackInfo(&buf[0], buf.size(), kTextSubtitles, "Picard", "fre"); +} + +TEST_F(WebMTracksParserTest, IgnoringTextTracks) { + InSequence s; + + TracksBuilder tb; + tb.AddTextTrack(1, 1, kWebMCodecSubtitles, "Subtitles", "fre"); + tb.AddTextTrack(2, 2, kWebMCodecSubtitles, "Commentary", "fre"); + + const std::vector buf = tb.Finish(); + scoped_ptr parser(new WebMTracksParser(media_log_, true)); + + EXPECT_MEDIA_LOG(HasSubstr("Ignoring text track 1")); + EXPECT_MEDIA_LOG(HasSubstr("Ignoring text track 2")); + + int result = parser->Parse(&buf[0], buf.size()); + EXPECT_GT(result, 0); + EXPECT_EQ(result, static_cast(buf.size())); + + EXPECT_EQ(parser->text_tracks().size(), 0u); + + const std::set& ignored_tracks = parser->ignored_tracks(); + EXPECT_TRUE(ignored_tracks.find(1) != ignored_tracks.end()); + EXPECT_TRUE(ignored_tracks.find(2) != ignored_tracks.end()); + + // Test again w/o ignoring the test tracks. + parser.reset(new WebMTracksParser(media_log_, false)); + + result = parser->Parse(&buf[0], buf.size()); + EXPECT_GT(result, 0); + + EXPECT_EQ(parser->ignored_tracks().size(), 0u); + EXPECT_EQ(parser->text_tracks().size(), 2u); +} + +TEST_F(WebMTracksParserTest, AudioVideoDefaultDurationUnset) { + // Other audio/video decoder config fields are necessary in the test + // audio/video TrackEntry configurations. This method does only very minimal + // verification of their inclusion and parsing; the goal is to confirm + // TrackEntry DefaultDuration defaults to -1 if not included in audio or + // video TrackEntry. + TracksBuilder tb; + tb.AddAudioTrack(1, 1, "A_VORBIS", "audio", "", -1, 2, 8000); + tb.AddVideoTrack(2, 2, "V_VP8", "video", "", -1, 320, 240); + const std::vector buf = tb.Finish(); + + scoped_ptr parser(new WebMTracksParser(media_log_, true)); + int result = parser->Parse(&buf[0], buf.size()); + EXPECT_LE(0, result); + EXPECT_EQ(static_cast(buf.size()), result); + + EXPECT_EQ(kNoTimestamp(), + parser->GetAudioDefaultDuration(kDefaultTimecodeScaleInUs)); + EXPECT_EQ(kNoTimestamp(), + parser->GetVideoDefaultDuration(kDefaultTimecodeScaleInUs)); + + const VideoDecoderConfig& video_config = parser->video_decoder_config(); + EXPECT_TRUE(video_config.IsValidConfig()); + EXPECT_EQ(320, video_config.coded_size().width()); + EXPECT_EQ(240, video_config.coded_size().height()); + + const AudioDecoderConfig& audio_config = parser->audio_decoder_config(); + EXPECT_TRUE(audio_config.IsValidConfig()); + EXPECT_EQ(CHANNEL_LAYOUT_STEREO, audio_config.channel_layout()); + EXPECT_EQ(8000, audio_config.samples_per_second()); +} + +TEST_F(WebMTracksParserTest, AudioVideoDefaultDurationSet) { + // Confirm audio or video TrackEntry DefaultDuration values are parsed, if + // present. + TracksBuilder tb; + tb.AddAudioTrack(1, 1, "A_VORBIS", "audio", "", 12345678, 2, 8000); + tb.AddVideoTrack(2, 2, "V_VP8", "video", "", 987654321, 320, 240); + const std::vector buf = tb.Finish(); + + scoped_ptr parser(new WebMTracksParser(media_log_, true)); + int result = parser->Parse(&buf[0], buf.size()); + EXPECT_LE(0, result); + EXPECT_EQ(static_cast(buf.size()), result); + + EXPECT_EQ(base::TimeDelta::FromMicroseconds(12000), + parser->GetAudioDefaultDuration(kDefaultTimecodeScaleInUs)); + EXPECT_EQ(base::TimeDelta::FromMicroseconds(985000), + parser->GetVideoDefaultDuration(5000.0)); // 5 ms resolution + EXPECT_EQ(kNoTimestamp(), parser->GetAudioDefaultDuration(12346.0)); + EXPECT_EQ(base::TimeDelta::FromMicroseconds(12345), + parser->GetAudioDefaultDuration(12345.0)); + EXPECT_EQ(base::TimeDelta::FromMicroseconds(12003), + parser->GetAudioDefaultDuration(1000.3)); // 1.0003 ms resolution +} + +TEST_F(WebMTracksParserTest, InvalidZeroDefaultDurationSet) { + // Confirm parse error if TrackEntry DefaultDuration is present, but is 0ns. + TracksBuilder tb(true); + tb.AddAudioTrack(1, 1, "A_VORBIS", "audio", "", 0, 2, 8000); + const std::vector buf = tb.Finish(); + + scoped_ptr parser(new WebMTracksParser(media_log_, true)); + + EXPECT_MEDIA_LOG(HasSubstr("Illegal 0ns audio TrackEntry DefaultDuration")); + + EXPECT_EQ(-1, parser->Parse(&buf[0], buf.size())); +} + +TEST_F(WebMTracksParserTest, HighTrackUID) { + // Confirm no parse error if TrackEntry TrackUID has MSb set + // (http://crbug.com/397067). + TracksBuilder tb(true); + tb.AddAudioTrack(1, 1ULL << 31, "A_VORBIS", "audio", "", 40, 2, 8000); + const std::vector buf = tb.Finish(); + + scoped_ptr parser(new WebMTracksParser(media_log_, true)); + EXPECT_GT(parser->Parse(&buf[0], buf.size()),0); +} + +} // namespace media diff --git a/packager/media/formats/webm/webm_video_client.cc b/packager/media/formats/webm/webm_video_client.cc new file mode 100644 index 0000000000..2d3f43b9ce --- /dev/null +++ b/packager/media/formats/webm/webm_video_client.cc @@ -0,0 +1,163 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "media/formats/webm/webm_video_client.h" + +#include "media/base/video_decoder_config.h" +#include "media/formats/webm/webm_constants.h" + +namespace media { + +WebMVideoClient::WebMVideoClient(const scoped_refptr& media_log) + : media_log_(media_log) { + Reset(); +} + +WebMVideoClient::~WebMVideoClient() { +} + +void WebMVideoClient::Reset() { + pixel_width_ = -1; + pixel_height_ = -1; + crop_bottom_ = -1; + crop_top_ = -1; + crop_left_ = -1; + crop_right_ = -1; + display_width_ = -1; + display_height_ = -1; + display_unit_ = -1; + alpha_mode_ = -1; +} + +bool WebMVideoClient::InitializeConfig( + const std::string& codec_id, const std::vector& codec_private, + bool is_encrypted, VideoDecoderConfig* config) { + DCHECK(config); + + VideoCodec video_codec = kUnknownVideoCodec; + VideoCodecProfile profile = VIDEO_CODEC_PROFILE_UNKNOWN; + if (codec_id == "V_VP8") { + video_codec = kCodecVP8; + profile = VP8PROFILE_ANY; + } else if (codec_id == "V_VP9") { + video_codec = kCodecVP9; + profile = VP9PROFILE_ANY; + } else { + MEDIA_LOG(ERROR, media_log_) << "Unsupported video codec_id " << codec_id; + return false; + } + + VideoPixelFormat format = + (alpha_mode_ == 1) ? PIXEL_FORMAT_YV12A : PIXEL_FORMAT_YV12; + + if (pixel_width_ <= 0 || pixel_height_ <= 0) + return false; + + // Set crop and display unit defaults if these elements are not present. + if (crop_bottom_ == -1) + crop_bottom_ = 0; + + if (crop_top_ == -1) + crop_top_ = 0; + + if (crop_left_ == -1) + crop_left_ = 0; + + if (crop_right_ == -1) + crop_right_ = 0; + + if (display_unit_ == -1) + display_unit_ = 0; + + gfx::Size coded_size(pixel_width_, pixel_height_); + gfx::Rect visible_rect(crop_top_, crop_left_, + pixel_width_ - (crop_left_ + crop_right_), + pixel_height_ - (crop_top_ + crop_bottom_)); + if (display_unit_ == 0) { + if (display_width_ <= 0) + display_width_ = visible_rect.width(); + if (display_height_ <= 0) + display_height_ = visible_rect.height(); + } else if (display_unit_ == 3) { + if (display_width_ <= 0 || display_height_ <= 0) + return false; + } else { + MEDIA_LOG(ERROR, media_log_) << "Unsupported display unit type " + << display_unit_; + return false; + } + gfx::Size natural_size = gfx::Size(display_width_, display_height_); + const uint8* extra_data = NULL; + size_t extra_data_size = 0; + if (codec_private.size() > 0) { + extra_data = &codec_private[0]; + extra_data_size = codec_private.size(); + } + + config->Initialize(video_codec, profile, format, COLOR_SPACE_HD_REC709, + coded_size, visible_rect, natural_size, extra_data, + extra_data_size, is_encrypted); + return config->IsValidConfig(); +} + +bool WebMVideoClient::OnUInt(int id, int64 val) { + int64* dst = NULL; + + switch (id) { + case kWebMIdPixelWidth: + dst = &pixel_width_; + break; + case kWebMIdPixelHeight: + dst = &pixel_height_; + break; + case kWebMIdPixelCropTop: + dst = &crop_top_; + break; + case kWebMIdPixelCropBottom: + dst = &crop_bottom_; + break; + case kWebMIdPixelCropLeft: + dst = &crop_left_; + break; + case kWebMIdPixelCropRight: + dst = &crop_right_; + break; + case kWebMIdDisplayWidth: + dst = &display_width_; + break; + case kWebMIdDisplayHeight: + dst = &display_height_; + break; + case kWebMIdDisplayUnit: + dst = &display_unit_; + break; + case kWebMIdAlphaMode: + dst = &alpha_mode_; + break; + default: + return true; + } + + if (*dst != -1) { + MEDIA_LOG(ERROR, media_log_) << "Multiple values for id " << std::hex << id + << " specified (" << *dst << " and " << val + << ")"; + return false; + } + + *dst = val; + return true; +} + +bool WebMVideoClient::OnBinary(int id, const uint8* data, int size) { + // Accept binary fields we don't care about for now. + return true; +} + +bool WebMVideoClient::OnFloat(int id, double val) { + // Accept float fields we don't care about for now. + return true; +} + +} // namespace media diff --git a/packager/media/formats/webm/webm_video_client.h b/packager/media/formats/webm/webm_video_client.h new file mode 100644 index 0000000000..f2f1df4c9f --- /dev/null +++ b/packager/media/formats/webm/webm_video_client.h @@ -0,0 +1,61 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef MEDIA_FORMATS_WEBM_WEBM_VIDEO_CLIENT_H_ +#define MEDIA_FORMATS_WEBM_WEBM_VIDEO_CLIENT_H_ + +#include +#include + +#include "media/base/media_log.h" +#include "media/formats/webm/webm_parser.h" + +namespace media { +class VideoDecoderConfig; + +// Helper class used to parse a Video element inside a TrackEntry element. +class WebMVideoClient : public WebMParserClient { + public: + explicit WebMVideoClient(const scoped_refptr& media_log); + ~WebMVideoClient() override; + + // Reset this object's state so it can process a new video track element. + void Reset(); + + // Initialize |config| with the data in |codec_id|, |codec_private|, + // |is_encrypted| and the fields parsed from the last video track element this + // object was used to parse. + // Returns true if |config| was successfully initialized. + // Returns false if there was unexpected values in the provided parameters or + // video track element fields. The contents of |config| are undefined in this + // case and should not be relied upon. + bool InitializeConfig(const std::string& codec_id, + const std::vector& codec_private, + bool is_encrypted, + VideoDecoderConfig* config); + + private: + // WebMParserClient implementation. + bool OnUInt(int id, int64 val) override; + bool OnBinary(int id, const uint8* data, int size) override; + bool OnFloat(int id, double val) override; + + scoped_refptr media_log_; + int64 pixel_width_; + int64 pixel_height_; + int64 crop_bottom_; + int64 crop_top_; + int64 crop_left_; + int64 crop_right_; + int64 display_width_; + int64 display_height_; + int64 display_unit_; + int64 alpha_mode_; + + DISALLOW_COPY_AND_ASSIGN(WebMVideoClient); +}; + +} // namespace media + +#endif // MEDIA_FORMATS_WEBM_WEBM_VIDEO_CLIENT_H_ diff --git a/packager/media/formats/webm/webm_webvtt_parser.cc b/packager/media/formats/webm/webm_webvtt_parser.cc new file mode 100644 index 0000000000..64de1ef443 --- /dev/null +++ b/packager/media/formats/webm/webm_webvtt_parser.cc @@ -0,0 +1,78 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "media/formats/webm/webm_webvtt_parser.h" + +namespace media { + +void WebMWebVTTParser::Parse(const uint8* payload, int payload_size, + std::string* id, + std::string* settings, + std::string* content) { + WebMWebVTTParser parser(payload, payload_size); + parser.Parse(id, settings, content); +} + +WebMWebVTTParser::WebMWebVTTParser(const uint8* payload, int payload_size) + : ptr_(payload), + ptr_end_(payload + payload_size) { +} + +void WebMWebVTTParser::Parse(std::string* id, + std::string* settings, + std::string* content) { + ParseLine(id); + ParseLine(settings); + content->assign(ptr_, ptr_end_); +} + +bool WebMWebVTTParser::GetByte(uint8* byte) { + if (ptr_ >= ptr_end_) + return false; // indicates end-of-stream + + *byte = *ptr_++; + return true; +} + +void WebMWebVTTParser::UngetByte() { + --ptr_; +} + +void WebMWebVTTParser::ParseLine(std::string* line) { + line->clear(); + + // Consume characters from the stream, until we reach end-of-line. + + // The WebVTT spec states that lines may be terminated in any of the following + // three ways: + // LF + // CR + // CR LF + + // The spec is here: + // http://wiki.webmproject.org/webm-metadata/temporal-metadata/webvtt-in-webm + + enum { + kLF = '\x0A', + kCR = '\x0D' + }; + + for (;;) { + uint8 byte; + + if (!GetByte(&byte) || byte == kLF) + return; + + if (byte == kCR) { + if (GetByte(&byte) && byte != kLF) + UngetByte(); + + return; + } + + line->push_back(byte); + } +} + +} // namespace media diff --git a/packager/media/formats/webm/webm_webvtt_parser.h b/packager/media/formats/webm/webm_webvtt_parser.h new file mode 100644 index 0000000000..12bbbd4dd2 --- /dev/null +++ b/packager/media/formats/webm/webm_webvtt_parser.h @@ -0,0 +1,49 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef MEDIA_FORMATS_WEBM_WEBM_WEBVTT_PARSER_H_ +#define MEDIA_FORMATS_WEBM_WEBM_WEBVTT_PARSER_H_ + +#include + +#include "base/basictypes.h" +#include "media/base/media_export.h" + +namespace media { + +class MEDIA_EXPORT WebMWebVTTParser { + public: + // Utility function to parse the WebVTT cue from a byte stream. + static void Parse(const uint8* payload, int payload_size, + std::string* id, + std::string* settings, + std::string* content); + + private: + // The payload is the embedded WebVTT cue, stored in a WebM block. + // The parser treats this as a UTF-8 byte stream. + WebMWebVTTParser(const uint8* payload, int payload_size); + + // Parse the cue identifier, settings, and content from the stream. + void Parse(std::string* id, std::string* settings, std::string* content); + // Remove a byte from the stream, advancing the stream pointer. + // Returns true if a character was returned; false means "end of stream". + bool GetByte(uint8* byte); + + // Backup the stream pointer. + void UngetByte(); + + // Parse a line of text from the stream. + void ParseLine(std::string* line); + + // Represents the portion of the stream that has not been consumed yet. + const uint8* ptr_; + const uint8* const ptr_end_; + + DISALLOW_COPY_AND_ASSIGN(WebMWebVTTParser); +}; + +} // namespace media + +#endif // MEDIA_FORMATS_WEBM_WEBM_WEBVTT_PARSER_H_ diff --git a/packager/media/formats/webm/webm_webvtt_parser_unittest.cc b/packager/media/formats/webm/webm_webvtt_parser_unittest.cc new file mode 100644 index 0000000000..ecdabd4297 --- /dev/null +++ b/packager/media/formats/webm/webm_webvtt_parser_unittest.cc @@ -0,0 +1,105 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "media/formats/webm/webm_webvtt_parser.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using ::testing::InSequence; + +namespace media { + +typedef std::vector Cue; + +static Cue EncodeCue(const std::string& id, + const std::string& settings, + const std::string& content) { + const std::string result = id + '\n' + settings + '\n' + content; + const uint8* const buf = reinterpret_cast(result.data()); + return Cue(buf, buf + result.length()); +} + +static void DecodeCue(const Cue& cue, + std::string* id, + std::string* settings, + std::string* content) { + WebMWebVTTParser::Parse(&cue[0], static_cast(cue.size()), + id, settings, content); +} + +class WebMWebVTTParserTest : public testing::Test { + public: + WebMWebVTTParserTest() {} +}; + +TEST_F(WebMWebVTTParserTest, Blank) { + InSequence s; + + const Cue cue = EncodeCue("", "", "Subtitle"); + std::string id, settings, content; + + DecodeCue(cue, &id, &settings, &content); + EXPECT_EQ(id, ""); + EXPECT_EQ(settings, ""); + EXPECT_EQ(content, "Subtitle"); +} + +TEST_F(WebMWebVTTParserTest, Id) { + InSequence s; + + for (int i = 1; i <= 9; ++i) { + const std::string idsrc(1, '0'+i); + const Cue cue = EncodeCue(idsrc, "", "Subtitle"); + std::string id, settings, content; + + DecodeCue(cue, &id, &settings, &content); + EXPECT_EQ(id, idsrc); + EXPECT_EQ(settings, ""); + EXPECT_EQ(content, "Subtitle"); + } +} + +TEST_F(WebMWebVTTParserTest, Settings) { + InSequence s; + + enum { kSettingsCount = 4 }; + const char* const settings_str[kSettingsCount] = { + "vertical:lr", + "line:50%", + "position:42%", + "vertical:rl line:42% position:100%" }; + + for (int i = 0; i < kSettingsCount; ++i) { + const Cue cue = EncodeCue("", settings_str[i], "Subtitle"); + std::string id, settings, content; + + DecodeCue(cue, &id, &settings, &content); + EXPECT_EQ(id, ""); + EXPECT_EQ(settings, settings_str[i]); + EXPECT_EQ(content, "Subtitle"); + } +} + +TEST_F(WebMWebVTTParserTest, Content) { + InSequence s; + + enum { kContentCount = 4 }; + const char* const content_str[kContentCount] = { + "Subtitle", + "Another Subtitle", + "Yet Another Subtitle", + "Another Subtitle\nSplit Across Two Lines" }; + + for (int i = 0; i < kContentCount; ++i) { + const Cue cue = EncodeCue("", "", content_str[i]); + std::string id, settings, content; + + DecodeCue(cue, &id, &settings, &content); + EXPECT_EQ(id, ""); + EXPECT_EQ(settings, ""); + EXPECT_EQ(content, content_str[i]); + } +} + +} // namespace media