Implemented Id3Tag class to handle ID3 tag

Also switched the original code in mp4 to use the new Id3Tag class.

Change-Id: I1db2c6c6142ed98b72a432980a6a54815f1a8cc4
This commit is contained in:
KongQun Yang 2018-05-07 15:57:56 -07:00
parent 8f3a45c497
commit 8333908df1
9 changed files with 286 additions and 97 deletions

View File

@ -0,0 +1,101 @@
// Copyright 2018 Google LLC. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
#include "packager/media/base/id3_tag.h"
#include "packager/base/logging.h"
#include "packager/media/base/buffer_writer.h"
#include "packager/media/base/fourccs.h"
namespace shaka {
namespace media {
namespace {
// ID3v2 header: http://id3.org/id3v2.4.0-structure.
const char kID3v2Identifier[] = "ID3";
const uint16_t kID3v2Version = 0x0400; // id3v2.4.0
const uint32_t kMaxSynchsafeSize = 0x0FFFFFFF; // 28 effective bits.
// Convert the specified size into synchsafe integer, where the most significant
// bit (bit 7) is set to zero in every byte.
uint32_t EncodeSynchsafe(uint32_t size) {
return (size & 0x7F) | (((size >> 7) & 0x7F) << 8) |
(((size >> 14) & 0x7F) << 16) | (((size >> 21) & 0x7F) << 24);
}
bool WriteId3v2Header(uint32_t frames_size, BufferWriter* buffer_writer) {
buffer_writer->AppendString(kID3v2Identifier);
buffer_writer->AppendInt(kID3v2Version);
const uint8_t flags = 0;
buffer_writer->AppendInt(flags);
if (frames_size > kMaxSynchsafeSize) {
LOG(ERROR) << "Input size (" << frames_size
<< ") is out of range (> max synchsafe integer "
<< kMaxSynchsafeSize << ").";
return false;
}
buffer_writer->AppendInt(EncodeSynchsafe(frames_size));
return true;
}
} // namespace
void Id3Tag::AddPrivateFrame(const std::string& owner,
const std::string& data) {
private_frames_.push_back({owner, data});
}
bool Id3Tag::WriteToBuffer(BufferWriter* buffer_writer) {
BufferWriter frames_buffer;
for (const PrivateFrame& private_frame : private_frames_) {
if (!WritePrivateFrame(private_frame, &frames_buffer))
return false;
}
if (!WriteId3v2Header(frames_buffer.Size(), buffer_writer))
return false;
buffer_writer->AppendBuffer(frames_buffer);
return true;
}
bool Id3Tag::WriteToVector(std::vector<uint8_t>* output) {
BufferWriter buffer_writer;
if (!WriteToBuffer(&buffer_writer))
return false;
buffer_writer.SwapBuffer(output);
return true;
}
// Implemented per http://id3.org/id3v2.4.0-frames 4.27.
bool Id3Tag::WritePrivateFrame(const PrivateFrame& private_frame,
BufferWriter* buffer_writer) {
buffer_writer->AppendInt(static_cast<uint32_t>(FOURCC_PRIV));
const uint32_t frame_size = static_cast<uint32_t>(
private_frame.owner.size() + 1 + private_frame.data.size());
if (frame_size > kMaxSynchsafeSize) {
LOG(ERROR) << "Input size (" << frame_size
<< ") is out of range (> max synchsafe integer "
<< kMaxSynchsafeSize << ").";
return false;
}
buffer_writer->AppendInt(EncodeSynchsafe(frame_size));
const uint16_t flags = 0;
buffer_writer->AppendInt(flags);
buffer_writer->AppendString(private_frame.owner);
uint8_t byte = 0; // NULL terminating byte between owner and value.
buffer_writer->AppendInt(byte);
buffer_writer->AppendString(private_frame.data);
return true;
}
} // namespace media
} // namespace shaka

View File

@ -0,0 +1,59 @@
// Copyright 2018 Google LLC. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
#ifndef PACKAGER_MEDIA_BASE_ID3_TAG_H_
#define PACKAGER_MEDIA_BASE_ID3_TAG_H_
#include <string>
#include <vector>
namespace shaka {
namespace media {
class BufferWriter;
/// Implements ID3 tag defined in: http://id3.org/.
/// Only PrivateFrame is supported right now.
class Id3Tag {
public:
Id3Tag() = default;
~Id3Tag() = default;
/// Add a "Private Frame".
/// See http://id3.org/id3v2.4.0-frames 4.27.
/// @owner contains the owner identifier.
/// @data contains the data for this private frame.
void AddPrivateFrame(const std::string& owner, const std::string& data);
/// Write the ID3 tag to a buffer.
/// @param buffer_writer points to the @a BufferWriter to write to.
/// @return true on success.
bool WriteToBuffer(BufferWriter* buffer_writer);
/// Write the ID3 tag to vector.
/// @param output points to the vector to write to.
/// @return true on success.
bool WriteToVector(std::vector<uint8_t>* output);
private:
Id3Tag(const Id3Tag&) = delete;
Id3Tag& operator=(const Id3Tag&) = delete;
struct PrivateFrame {
std::string owner;
std::string data;
};
bool WritePrivateFrame(const PrivateFrame& private_frame,
BufferWriter* buffer_writer);
std::vector<PrivateFrame> private_frames_;
};
} // namespace media
} // namespace shaka
#endif // PACKAGER_MEDIA_BASE_ID3_TAG_H_

View File

@ -0,0 +1,105 @@
// Copyright 2018 Google LLC. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
#include "packager/media/base/id3_tag.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "packager/media/base/buffer_writer.h"
using ::testing::ElementsAreArray;
namespace shaka {
namespace media {
TEST(Id3TagTest, WriteToVector) {
Id3Tag id3_tag;
std::vector<uint8_t> output;
id3_tag.WriteToVector(&output);
const uint8_t kExpectedOutput[] = {
'I', 'D', '3', 4, 0, 0, 0, 0, 0, 0,
};
EXPECT_THAT(output, ElementsAreArray(kExpectedOutput));
}
TEST(Id3TagTest, WriteToBuffer) {
Id3Tag id3_tag;
BufferWriter buffer_writer;
id3_tag.WriteToBuffer(&buffer_writer);
const uint8_t kExpectedOutput[] = {
'I', 'D', '3', 4, 0, 0, 0, 0, 0, 0,
};
EXPECT_EQ(
std::vector<uint8_t>(std::begin(kExpectedOutput),
std::end(kExpectedOutput)),
std::vector<uint8_t>(buffer_writer.Buffer(),
buffer_writer.Buffer() + buffer_writer.Size()));
}
TEST(Id3TagTest, AddPrivateFrameOnce) {
Id3Tag id3_tag;
id3_tag.AddPrivateFrame("testing.owner", "data");
std::vector<uint8_t> output;
id3_tag.WriteToVector(&output);
const uint8_t kExpectedOutput[] = {
'I', 'D', '3', 4, 0, 0, 0, 0, 0, 28, // Header
'P', 'R', 'I', 'V', 0, 0, 0, 18, 0, 0, 't', 'e', 's', 't',
'i', 'n', 'g', '.', 'o', 'w', 'n', 'e', 'r', 0, 'd', 'a', 't', 'a',
};
EXPECT_THAT(output, ElementsAreArray(kExpectedOutput));
}
TEST(Id3TagTest, AddPrivateFrameTwice) {
Id3Tag id3_tag;
id3_tag.AddPrivateFrame("testing.owner1", "data1");
id3_tag.AddPrivateFrame("testing.owner2", "data2");
std::vector<uint8_t> output;
id3_tag.WriteToVector(&output);
const uint8_t kExpectedOutput[] = {
'I', 'D', '3', 4, 0, 0, 0, 0, 0, 60, // Header
'P', 'R', 'I', 'V', 0, 0, 0, 20, 0, 0, 't', 'e', 's', 't', 'i',
'n', 'g', '.', 'o', 'w', 'n', 'e', 'r', '1', 0, 'd', 'a', 't', 'a', '1',
'P', 'R', 'I', 'V', 0, 0, 0, 20, 0, 0, 't', 'e', 's', 't', 'i',
'n', 'g', '.', 'o', 'w', 'n', 'e', 'r', '2', 0, 'd', 'a', 't', 'a', '2',
};
EXPECT_THAT(output, ElementsAreArray(kExpectedOutput));
}
TEST(Id3TagTest, AddBigPrivateFrameOnce) {
const size_t kTestOwnerSize = 200;
const size_t kTestDataSize = 200;
std::string test_owner(kTestOwnerSize, 0);
std::string test_data(kTestDataSize, 0);
Id3Tag id3_tag;
id3_tag.AddPrivateFrame(test_owner, test_data);
std::vector<uint8_t> output;
id3_tag.WriteToVector(&output);
const uint8_t kExpectedOutputHead[] = {
'I', 'D', '3', 4, 0, 0, 0, 0, 3, 27, // Header
'P', 'R', 'I', 'V', 0, 0, 3, 17, 0, 0,
};
std::vector<uint8_t> expected_output(std::begin(kExpectedOutputHead),
std::end(kExpectedOutputHead));
expected_output.resize(expected_output.size() + kTestOwnerSize + 1 +
kTestDataSize);
EXPECT_EQ(expected_output, output);
}
} // namespace media
} // namespace shaka

View File

@ -47,6 +47,8 @@
'fourccs.h',
'http_key_fetcher.cc',
'http_key_fetcher.h',
'id3_tag.cc',
'id3_tag.h',
'key_fetcher.cc',
'key_fetcher.h',
'key_source.cc',
@ -171,6 +173,7 @@
'container_names_unittest.cc',
'decryptor_source_unittest.cc',
'http_key_fetcher_unittest.cc',
'id3_tag_unittest.cc',
'muxer_util_unittest.cc',
'offset_byte_queue_unittest.cc',
'producer_consumer_queue_unittest.cc',

View File

@ -67,11 +67,6 @@ bool IsIvSizeValid(uint8_t per_sample_iv_size) {
// bit(5) Reserved // 0
const uint8_t kDdtsExtraData[] = {0xe4, 0x7c, 0, 4, 0, 0x0f, 0};
// ID3v2 header: http://id3.org/id3v2.4.0-structure
const uint32_t kID3v2HeaderSize = 10;
const char kID3v2Identifier[] = "ID3";
const uint16_t kID3v2Version = 0x0400; // id3v2.4.0
// Utility functions to check if the 64bit integers can fit in 32bit integer.
bool IsFitIn32Bits(uint64_t a) {
return a <= std::numeric_limits<uint32_t>::max();
@ -1328,84 +1323,24 @@ uint32_t Language::ComputeSize() const {
return 2;
}
bool PrivFrame::ReadWrite(BoxBuffer* buffer) {
FourCC fourcc = FOURCC_PRIV;
RCHECK(buffer->ReadWriteFourCC(&fourcc));
if (fourcc != FOURCC_PRIV) {
VLOG(1) << "Skip unrecognized id3 frame during read: "
<< FourCCToString(fourcc);
return true;
}
uint32_t frame_size = static_cast<uint32_t>(owner.size() + 1 + value.size());
// size should be encoded as synchsafe integer, which is not support here.
// We don't expect frame_size to be larger than 0x7F. Synchsafe integers less
// than 0x7F is encoded in the same way as normal integer.
DCHECK_LT(frame_size, 0x7Fu);
uint16_t flags = 0;
RCHECK(buffer->ReadWriteUInt32(&frame_size) &&
buffer->ReadWriteUInt16(&flags));
if (buffer->Reading()) {
std::string str;
RCHECK(buffer->ReadWriteString(&str, frame_size));
// |owner| is null terminated.
size_t pos = str.find('\0');
RCHECK(pos < str.size());
owner = str.substr(0, pos);
value = str.substr(pos + 1);
} else {
uint8_t byte = 0; // Null terminating byte between owner and value.
RCHECK(buffer->ReadWriteString(&owner, owner.size()) &&
buffer->ReadWriteUInt8(&byte) &&
buffer->ReadWriteString(&value, value.size()));
}
return true;
}
uint32_t PrivFrame::ComputeSize() const {
if (owner.empty() && value.empty())
return 0;
const uint32_t kFourCCSize = 4;
return kFourCCSize +
static_cast<uint32_t>(sizeof(uint32_t) + sizeof(uint16_t) +
owner.size() + 1 + value.size());
}
ID3v2::ID3v2() {}
ID3v2::~ID3v2() {}
FourCC ID3v2::BoxType() const { return FOURCC_ID32; }
bool ID3v2::ReadWriteInternal(BoxBuffer* buffer) {
RCHECK(ReadWriteHeaderInternal(buffer) &&
language.ReadWrite(buffer));
// Read/Write ID3v2 header
std::string id3v2_identifier = kID3v2Identifier;
uint16_t version = kID3v2Version;
// We only support PrivateFrame in ID3.
uint32_t data_size = private_frame.ComputeSize();
// size should be encoded as synchsafe integer, which is not support here.
// We don't expect data_size to be larger than 0x7F. Synchsafe integers less
// than 0x7F is encoded in the same way as normal integer.
DCHECK_LT(data_size, 0x7Fu);
uint8_t flags = 0;
RCHECK(buffer->ReadWriteString(&id3v2_identifier, id3v2_identifier.size()) &&
buffer->ReadWriteUInt16(&version) &&
buffer->ReadWriteUInt8(&flags) &&
buffer->ReadWriteUInt32(&data_size));
RCHECK(private_frame.ReadWrite(buffer));
RCHECK(ReadWriteHeaderInternal(buffer) && language.ReadWrite(buffer) &&
buffer->ReadWriteVector(&id3v2_data, buffer->Reading()
? buffer->BytesLeft()
: id3v2_data.size()));
return true;
}
size_t ID3v2::ComputeSizeInternal() {
uint32_t private_frame_size = private_frame.ComputeSize();
// Skip ID3v2 box generation if there is no private frame.
return private_frame_size == 0 ? 0 : HeaderSize() + language.ComputeSize() +
kID3v2HeaderSize +
private_frame_size;
// Skip ID3v2 box generation if there is no id3 data.
return id3v2_data.size() == 0
? 0
: HeaderSize() + language.ComputeSize() + id3v2_data.size();
}
Metadata::Metadata() {}

View File

@ -237,24 +237,12 @@ struct Language {
std::string code;
};
/// Implemented per http://id3.org/id3v2.4.0-frames.
struct PrivFrame {
bool ReadWrite(BoxBuffer* buffer);
uint32_t ComputeSize() const;
std::string owner;
std::string value;
};
/// Implemented per http://mp4ra.org/specs.html#id3v2 and
/// http://id3.org/id3v2.4.0-structure.
/// Implemented per http://mp4ra.org/#/references.
struct ID3v2 : FullBox {
DECLARE_BOX_METHODS(ID3v2);
Language language;
/// We only support PrivateFrame in ID3. Other frames are ignored.
PrivFrame private_frame;
std::vector<uint8_t> id3v2_data;
};
struct Metadata : FullBox {

View File

@ -229,12 +229,8 @@ inline bool operator==(const Language& lhs,
return lhs.code == rhs.code;
}
inline bool operator==(const PrivFrame& lhs, const PrivFrame& rhs) {
return lhs.owner == rhs.owner && lhs.value == rhs.value;
}
inline bool operator==(const ID3v2& lhs, const ID3v2& rhs) {
return lhs.language == rhs.language && lhs.private_frame == rhs.private_frame;
return lhs.language == rhs.language && lhs.id3v2_data == rhs.id3v2_data;
}
inline bool operator==(const Metadata& lhs, const Metadata& rhs) {

View File

@ -309,13 +309,12 @@ class BoxDefinitionsTestGeneral : public testing::Test {
void Fill(ID3v2* id3v2) {
id3v2->language.code = "eng";
id3v2->private_frame.owner = "shaka-packager";
id3v2->private_frame.value = "version 1.2.0-debug";
id3v2->id3v2_data.assign(std::begin(kData16Bytes), std::end(kData16Bytes));
}
void Modify(ID3v2* id3v2) {
id3v2->language.code = "fre";
id3v2->private_frame.value = "version 1.3.1-release";
id3v2->id3v2_data.assign(std::begin(kData8Bytes), std::end(kData8Bytes));
}
void Fill(Metadata* metadata) {

View File

@ -10,6 +10,7 @@
#include "packager/base/logging.h"
#include "packager/media/base/buffer_writer.h"
#include "packager/media/base/id3_tag.h"
#include "packager/media/base/media_sample.h"
#include "packager/media/base/muxer_options.h"
#include "packager/media/base/muxer_util.h"
@ -92,8 +93,10 @@ Status Segmenter::Initialize(
if (!version.empty()) {
moov_->metadata.handler.handler_type = FOURCC_ID32;
moov_->metadata.id3v2.language.code = "eng";
moov_->metadata.id3v2.private_frame.owner = GetPackagerProjectUrl();
moov_->metadata.id3v2.private_frame.value = version;
Id3Tag id3_tag;
id3_tag.AddPrivateFrame(GetPackagerProjectUrl(), version);
CHECK(id3_tag.WriteToVector(&moov_->metadata.id3v2.id3v2_data));
}
return DoInitialize();
}