Support for MPEG-1 Audio in mpeg2ts I/O and packed-audio output (#778)
Implemented according to https://www.datavoyage.com/mpgscript/mpeghdr.htm. Closes #779.
This commit is contained in:
parent
15934d66c0
commit
98a9d1baf6
1
AUTHORS
1
AUTHORS
|
@ -17,6 +17,7 @@ Alen Vrecko <alen.vrecko@gmail.com>
|
||||||
Anders Hasselqvist <anders.hasselqvist@gmail.com>
|
Anders Hasselqvist <anders.hasselqvist@gmail.com>
|
||||||
Chun-da Chen <capitalm.c@gmail.com>
|
Chun-da Chen <capitalm.c@gmail.com>
|
||||||
Daniel Cantarín <canta@canta.com.ar>
|
Daniel Cantarín <canta@canta.com.ar>
|
||||||
|
Evgeny Zajcev <zevlg@yandex.ru>
|
||||||
Google Inc. <*@google.com>
|
Google Inc. <*@google.com>
|
||||||
Ivi.ru LLC <*@ivi.ru>
|
Ivi.ru LLC <*@ivi.ru>
|
||||||
Leandro Moreira <leandro.ribeiro.moreira@gmail.com>
|
Leandro Moreira <leandro.ribeiro.moreira@gmail.com>
|
||||||
|
|
|
@ -28,6 +28,7 @@ Bei Li <beil@google.com>
|
||||||
Chun-da Chen <capitalm.c@gmail.com>
|
Chun-da Chen <capitalm.c@gmail.com>
|
||||||
Daniel Cantarín <canta@canta.com.ar>
|
Daniel Cantarín <canta@canta.com.ar>
|
||||||
David Cavar <pal3thorn@gmail.com>
|
David Cavar <pal3thorn@gmail.com>
|
||||||
|
Evgeny Zajcev <zevlg@yandex.ru>
|
||||||
Gabe Kopley <gabe@philo.com>
|
Gabe Kopley <gabe@philo.com>
|
||||||
Haoming Chen <hmchen@google.com>
|
Haoming Chen <hmchen@google.com>
|
||||||
Jacob Trimble <modmaker@google.com>
|
Jacob Trimble <modmaker@google.com>
|
||||||
|
|
|
@ -40,6 +40,7 @@ Shaka Packager supports:
|
||||||
| VP9 | I / O | I / O | - | - | - |
|
| VP9 | I / O | I / O | - | - | - |
|
||||||
| AV1 | I / O | I / O | - | - | - |
|
| AV1 | I / O | I / O | - | - | - |
|
||||||
| AAC | I / O | - | I / O | I | O |
|
| AAC | I / O | - | I / O | I | O |
|
||||||
|
| MP3 | - | - | I / O | - | O |
|
||||||
| Dolby AC3 | I / O | - | I / O | - | O |
|
| Dolby AC3 | I / O | - | I / O | - | O |
|
||||||
| Dolby EAC3 | I / O | - | O | - | O |
|
| Dolby EAC3 | I / O | - | O | - | O |
|
||||||
| DTS | I / O | - | - | - | - |
|
| DTS | I / O | - | - | - | - |
|
||||||
|
|
|
@ -40,6 +40,7 @@ std::shared_ptr<Muxer> MuxerFactory::CreateMuxer(
|
||||||
|
|
||||||
switch (output_format) {
|
switch (output_format) {
|
||||||
case CONTAINER_AAC:
|
case CONTAINER_AAC:
|
||||||
|
case CONTAINER_MP3:
|
||||||
case CONTAINER_AC3:
|
case CONTAINER_AC3:
|
||||||
case CONTAINER_EAC3:
|
case CONTAINER_EAC3:
|
||||||
muxer = std::make_shared<PackedAudioWriter>(options);
|
muxer = std::make_shared<PackedAudioWriter>(options);
|
||||||
|
|
|
@ -43,6 +43,8 @@ std::string AudioCodecToString(Codec codec) {
|
||||||
return "Opus";
|
return "Opus";
|
||||||
case kCodecVorbis:
|
case kCodecVorbis:
|
||||||
return "Vorbis";
|
return "Vorbis";
|
||||||
|
case kCodecMP3:
|
||||||
|
return "MP3";
|
||||||
default:
|
default:
|
||||||
NOTIMPLEMENTED() << "Unknown Audio Codec: " << codec;
|
NOTIMPLEMENTED() << "Unknown Audio Codec: " << codec;
|
||||||
return "UnknownCodec";
|
return "UnknownCodec";
|
||||||
|
@ -123,6 +125,8 @@ std::string AudioStreamInfo::GetCodecString(Codec codec,
|
||||||
return "flac";
|
return "flac";
|
||||||
case kCodecOpus:
|
case kCodecOpus:
|
||||||
return "opus";
|
return "opus";
|
||||||
|
case kCodecMP3:
|
||||||
|
return "mp3";
|
||||||
case kCodecVorbis:
|
case kCodecVorbis:
|
||||||
return "vorbis";
|
return "vorbis";
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -1734,6 +1734,8 @@ MediaContainerName DetermineContainerFromFormatName(
|
||||||
} else if (base::EqualsCaseInsensitiveASCII(format_name, "ec3") ||
|
} else if (base::EqualsCaseInsensitiveASCII(format_name, "ec3") ||
|
||||||
base::EqualsCaseInsensitiveASCII(format_name, "eac3")) {
|
base::EqualsCaseInsensitiveASCII(format_name, "eac3")) {
|
||||||
return CONTAINER_EAC3;
|
return CONTAINER_EAC3;
|
||||||
|
} else if (base::EqualsCaseInsensitiveASCII(format_name, "mp3")) {
|
||||||
|
return CONTAINER_MP3;
|
||||||
} else if (base::EqualsCaseInsensitiveASCII(format_name, "webm")) {
|
} else if (base::EqualsCaseInsensitiveASCII(format_name, "webm")) {
|
||||||
return CONTAINER_WEBM;
|
return CONTAINER_WEBM;
|
||||||
} else if (base::EqualsCaseInsensitiveASCII(format_name, "cmfa") ||
|
} else if (base::EqualsCaseInsensitiveASCII(format_name, "cmfa") ||
|
||||||
|
|
|
@ -90,6 +90,7 @@ enum FourCC : uint32_t {
|
||||||
FOURCC_minf = 0x6d696e66,
|
FOURCC_minf = 0x6d696e66,
|
||||||
FOURCC_moof = 0x6d6f6f66,
|
FOURCC_moof = 0x6d6f6f66,
|
||||||
FOURCC_moov = 0x6d6f6f76,
|
FOURCC_moov = 0x6d6f6f76,
|
||||||
|
FOURCC_mp3a = 0x6d703361,
|
||||||
FOURCC_mp41 = 0x6d703431,
|
FOURCC_mp41 = 0x6d703431,
|
||||||
FOURCC_mp4a = 0x6d703461,
|
FOURCC_mp4a = 0x6d703461,
|
||||||
FOURCC_mp4v = 0x6d703476,
|
FOURCC_mp4v = 0x6d703476,
|
||||||
|
|
|
@ -52,6 +52,7 @@ enum Codec {
|
||||||
kCodecFlac,
|
kCodecFlac,
|
||||||
kCodecOpus,
|
kCodecOpus,
|
||||||
kCodecVorbis,
|
kCodecVorbis,
|
||||||
|
kCodecMP3,
|
||||||
kCodecAudioMaxPlusOne,
|
kCodecAudioMaxPlusOne,
|
||||||
|
|
||||||
kCodecText = 300,
|
kCodecText = 300,
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
#include "packager/media/formats/mp2t/ac3_header.h"
|
#include "packager/media/formats/mp2t/ac3_header.h"
|
||||||
#include "packager/media/formats/mp2t/adts_header.h"
|
#include "packager/media/formats/mp2t/adts_header.h"
|
||||||
#include "packager/media/formats/mp2t/mp2t_common.h"
|
#include "packager/media/formats/mp2t/mp2t_common.h"
|
||||||
|
#include "packager/media/formats/mp2t/mpeg1_header.h"
|
||||||
#include "packager/media/formats/mp2t/ts_stream_type.h"
|
#include "packager/media/formats/mp2t/ts_stream_type.h"
|
||||||
|
|
||||||
namespace shaka {
|
namespace shaka {
|
||||||
|
@ -98,6 +99,8 @@ EsParserAudio::EsParserAudio(uint32_t pid,
|
||||||
sbr_in_mimetype_(sbr_in_mimetype) {
|
sbr_in_mimetype_(sbr_in_mimetype) {
|
||||||
if (stream_type == TsStreamType::kAc3) {
|
if (stream_type == TsStreamType::kAc3) {
|
||||||
audio_header_.reset(new Ac3Header);
|
audio_header_.reset(new Ac3Header);
|
||||||
|
} else if (stream_type == TsStreamType::kMpeg1Audio) {
|
||||||
|
audio_header_.reset(new Mpeg1Header);
|
||||||
} else {
|
} else {
|
||||||
DCHECK_EQ(stream_type, TsStreamType::kAdtsAac);
|
DCHECK_EQ(stream_type, TsStreamType::kAdtsAac);
|
||||||
audio_header_.reset(new AdtsHeader);
|
audio_header_.reset(new AdtsHeader);
|
||||||
|
@ -214,7 +217,9 @@ bool EsParserAudio::UpdateAudioConfiguration(const AudioHeader& audio_header) {
|
||||||
: samples_per_second;
|
: samples_per_second;
|
||||||
|
|
||||||
const Codec codec =
|
const Codec codec =
|
||||||
stream_type_ == TsStreamType::kAc3 ? kCodecAC3 : kCodecAAC;
|
stream_type_ == TsStreamType::kAc3
|
||||||
|
? kCodecAC3
|
||||||
|
: (stream_type_ == TsStreamType::kMpeg1Audio ? kCodecMP3 : kCodecAAC);
|
||||||
last_audio_decoder_config_ = std::make_shared<AudioStreamInfo>(
|
last_audio_decoder_config_ = std::make_shared<AudioStreamInfo>(
|
||||||
pid(), kMpeg2Timescale, kInfiniteDuration, codec,
|
pid(), kMpeg2Timescale, kInfiniteDuration, codec,
|
||||||
AudioStreamInfo::GetCodecString(codec, audio_header.GetObjectType()),
|
AudioStreamInfo::GetCodecString(codec, audio_header.GetObjectType()),
|
||||||
|
|
|
@ -31,6 +31,8 @@
|
||||||
'es_parser.h',
|
'es_parser.h',
|
||||||
'mp2t_media_parser.cc',
|
'mp2t_media_parser.cc',
|
||||||
'mp2t_media_parser.h',
|
'mp2t_media_parser.h',
|
||||||
|
'mpeg1_header.cc',
|
||||||
|
'mpeg1_header.h',
|
||||||
'pes_packet.cc',
|
'pes_packet.cc',
|
||||||
'pes_packet.h',
|
'pes_packet.h',
|
||||||
'pes_packet_generator.cc',
|
'pes_packet_generator.cc',
|
||||||
|
@ -72,6 +74,7 @@
|
||||||
'es_parser_h264_unittest.cc',
|
'es_parser_h264_unittest.cc',
|
||||||
'es_parser_h26x_unittest.cc',
|
'es_parser_h26x_unittest.cc',
|
||||||
'mp2t_media_parser_unittest.cc',
|
'mp2t_media_parser_unittest.cc',
|
||||||
|
'mpeg1_header_unittest.cc',
|
||||||
'pes_packet_generator_unittest.cc',
|
'pes_packet_generator_unittest.cc',
|
||||||
'program_map_table_writer_unittest.cc',
|
'program_map_table_writer_unittest.cc',
|
||||||
'ts_segmenter_unittest.cc',
|
'ts_segmenter_unittest.cc',
|
||||||
|
|
|
@ -298,6 +298,7 @@ void Mp2tMediaParser::RegisterPes(int pmt_pid,
|
||||||
base::Bind(&Mp2tMediaParser::OnEmitSample, base::Unretained(this))));
|
base::Bind(&Mp2tMediaParser::OnEmitSample, base::Unretained(this))));
|
||||||
break;
|
break;
|
||||||
case TsStreamType::kAdtsAac:
|
case TsStreamType::kAdtsAac:
|
||||||
|
case TsStreamType::kMpeg1Audio:
|
||||||
case TsStreamType::kAc3:
|
case TsStreamType::kAc3:
|
||||||
es_parser.reset(new EsParserAudio(
|
es_parser.reset(new EsParserAudio(
|
||||||
pes_pid, static_cast<TsStreamType>(stream_type),
|
pes_pid, static_cast<TsStreamType>(stream_type),
|
||||||
|
|
|
@ -0,0 +1,234 @@
|
||||||
|
#include "packager/media/formats/mp2t/mpeg1_header.h"
|
||||||
|
|
||||||
|
#include "packager/media/base/bit_reader.h"
|
||||||
|
#include "packager/media/base/bit_writer.h"
|
||||||
|
#include "packager/media/formats/mp2t/mp2t_common.h"
|
||||||
|
|
||||||
|
// Parsing is done according to
|
||||||
|
// https://www.datavoyage.com/mpgscript/mpeghdr.htm
|
||||||
|
namespace {
|
||||||
|
const size_t kMpeg1HeaderMinSize = 4;
|
||||||
|
|
||||||
|
const uint8_t kMpeg1V_INV = 0b01; /* Invalid version */
|
||||||
|
const uint8_t kMpeg1L_INV = 0b00; /* Invalid layer */
|
||||||
|
|
||||||
|
const uint8_t kMpeg1L_3 = 0b01;
|
||||||
|
const uint8_t kMpeg1L_2 = 0b10;
|
||||||
|
const uint8_t kMpeg1L_1 = 0b11;
|
||||||
|
|
||||||
|
const size_t kMpeg1SamplesPerFrameTable[] = {
|
||||||
|
// L1, L2, L3
|
||||||
|
384, 1152, 1152};
|
||||||
|
|
||||||
|
const uint32_t kMpeg1SampleRateTable[][3] = {
|
||||||
|
// clang-format off
|
||||||
|
// V1, V2, V2.5
|
||||||
|
{44100, 22050, 11025},
|
||||||
|
{48000, 24000, 12000},
|
||||||
|
{32000, 16000, 8000}};
|
||||||
|
// clang-format on
|
||||||
|
const size_t kMpeg1SampleRateTableSize = arraysize(kMpeg1SampleRateTable);
|
||||||
|
|
||||||
|
static inline uint32_t Mpeg1SampleRate(uint8_t sr_idx, uint8_t version) {
|
||||||
|
static int sr_version_indexes[] = {2, -1, 1, 0}; // {V2.5, RESERVED, V2, V1}
|
||||||
|
DCHECK_NE(version, 1);
|
||||||
|
DCHECK_LT(sr_idx, kMpeg1SampleRateTableSize);
|
||||||
|
return kMpeg1SampleRateTable[sr_idx][sr_version_indexes[version]];
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t kMpeg1BitrateTable[][5] = {
|
||||||
|
// clang-format off
|
||||||
|
// V1:L1, V1:L2, V1:L3, V2:L1, V2&V2.5:L2&L3
|
||||||
|
{ 0, 0, 0, 0, 0},
|
||||||
|
{ 32, 32, 32, 32, 8},
|
||||||
|
{ 64, 48, 40, 48, 16},
|
||||||
|
{ 96, 56, 48, 56, 24},
|
||||||
|
{ 128, 64, 56, 64, 32},
|
||||||
|
{ 160, 80, 64, 80, 40},
|
||||||
|
{ 192, 96, 80, 96, 48},
|
||||||
|
{ 224, 112, 96, 112, 56},
|
||||||
|
{ 256, 128, 112, 128, 64},
|
||||||
|
{ 288, 160, 128, 144, 80},
|
||||||
|
{ 320, 192, 160, 160, 96},
|
||||||
|
{ 352, 224, 192, 176, 112},
|
||||||
|
{ 384, 256, 224, 192, 128},
|
||||||
|
{ 416, 320, 256, 224, 144},
|
||||||
|
{ 448, 384, 320, 256, 160}};
|
||||||
|
// clang-format on
|
||||||
|
const size_t kMpeg1BitrateTableSize = arraysize(kMpeg1BitrateTable);
|
||||||
|
|
||||||
|
static inline uint32_t Mpeg1BitRate(uint8_t btr_idx,
|
||||||
|
uint8_t version,
|
||||||
|
uint8_t layer) {
|
||||||
|
static int btr_version_indexes[] = {1, -1, 1, 0}; // {V2.5, RESERVED, V2, V1}
|
||||||
|
static int btr_layer_indexes[] = {-1, 2, 1, 0}; // {RESERVED, L3, L2, L1}
|
||||||
|
|
||||||
|
DCHECK_NE(version, 1);
|
||||||
|
DCHECK_NE(layer, 0);
|
||||||
|
int vidx = btr_version_indexes[version];
|
||||||
|
int lidx = btr_layer_indexes[layer];
|
||||||
|
if (vidx == 1 && lidx > 1)
|
||||||
|
lidx = 1;
|
||||||
|
|
||||||
|
DCHECK_LT(vidx * 3 + lidx, 5);
|
||||||
|
DCHECK_LT(btr_idx, kMpeg1BitrateTableSize);
|
||||||
|
return kMpeg1BitrateTable[btr_idx][vidx * 3 + lidx] * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline size_t Mpeg1FrameSize(uint8_t layer,
|
||||||
|
uint32_t bitrate,
|
||||||
|
uint32_t sample_rate,
|
||||||
|
uint8_t padded) {
|
||||||
|
DCHECK_GT(sample_rate, static_cast<uint32_t>(0));
|
||||||
|
if (layer == kMpeg1L_1)
|
||||||
|
return (12 * bitrate / sample_rate + padded) * 4;
|
||||||
|
return 144 * bitrate / sample_rate + padded;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace shaka {
|
||||||
|
namespace media {
|
||||||
|
namespace mp2t {
|
||||||
|
|
||||||
|
bool Mpeg1Header::IsSyncWord(const uint8_t* buf) const {
|
||||||
|
return (buf[0] == 0xff) &&
|
||||||
|
((buf[1] & 0b11100000) == 0b11100000)
|
||||||
|
// Version 01 is reserved
|
||||||
|
&& ((buf[1] & 0b00011000) != 0b00001000)
|
||||||
|
// Layer 00 is reserved
|
||||||
|
&& ((buf[1] & 0b00000110) != 0b00000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t Mpeg1Header::GetMinFrameSize() const {
|
||||||
|
return kMpeg1HeaderMinSize + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t Mpeg1Header::GetSamplesPerFrame() const {
|
||||||
|
static int spf_layer_indexes[] = {-1, 2, 1, 0}; // {RESERVED, L3, L2, L1}
|
||||||
|
DCHECK_NE(layer_, 0);
|
||||||
|
return kMpeg1SamplesPerFrameTable[spf_layer_indexes[layer_]];
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Mpeg1Header::Parse(const uint8_t* mpeg1_frame, size_t mpeg1_frame_size) {
|
||||||
|
DCHECK(mpeg1_frame);
|
||||||
|
|
||||||
|
if (mpeg1_frame_size < kMpeg1HeaderMinSize)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
BitReader frame(mpeg1_frame, mpeg1_frame_size);
|
||||||
|
// Verify frame starts with sync bits (0x7ff).
|
||||||
|
uint32_t sync;
|
||||||
|
RCHECK(frame.ReadBits(11, &sync));
|
||||||
|
RCHECK(sync == 0x7ff);
|
||||||
|
// MPEG version and layer.
|
||||||
|
RCHECK(frame.ReadBits(2, &version_));
|
||||||
|
RCHECK(version_ != kMpeg1V_INV);
|
||||||
|
RCHECK(frame.ReadBits(2, &layer_));
|
||||||
|
RCHECK(layer_ != kMpeg1L_INV);
|
||||||
|
RCHECK(frame.ReadBits(1, &protection_absent_));
|
||||||
|
|
||||||
|
uint8_t btr_idx;
|
||||||
|
RCHECK(frame.ReadBits(4, &btr_idx));
|
||||||
|
RCHECK(btr_idx > 0);
|
||||||
|
bitrate_ = Mpeg1BitRate(btr_idx, version_, layer_);
|
||||||
|
|
||||||
|
uint8_t sr_idx;
|
||||||
|
RCHECK(frame.ReadBits(2, &sr_idx));
|
||||||
|
RCHECK(sr_idx < 0b11);
|
||||||
|
sample_rate_ = Mpeg1SampleRate(sr_idx, version_);
|
||||||
|
|
||||||
|
RCHECK(frame.ReadBits(1, &padded_));
|
||||||
|
// Skip private stream bit.
|
||||||
|
RCHECK(frame.SkipBits(1));
|
||||||
|
|
||||||
|
RCHECK(frame.ReadBits(2, &channel_mode_));
|
||||||
|
// Skip Mode extension
|
||||||
|
RCHECK(frame.SkipBits(2));
|
||||||
|
// Skip copyright, origination and emphasis info.
|
||||||
|
RCHECK(frame.SkipBits(4));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t Mpeg1Header::GetHeaderSize() const {
|
||||||
|
// Unlike ADTS, for MP3, the whole frame is included in the media sample, so
|
||||||
|
// return 0 header size.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t Mpeg1Header::GetFrameSize() const {
|
||||||
|
return Mpeg1FrameSize(layer_, bitrate_, sample_rate_, padded_);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t Mpeg1Header::GetFrameSizeWithoutParsing(const uint8_t* data,
|
||||||
|
size_t num_bytes) const {
|
||||||
|
DCHECK_GT(num_bytes, static_cast<size_t>(2));
|
||||||
|
uint8_t version = (data[1] & 0b00011000) >> 3;
|
||||||
|
uint8_t layer = (data[1] & 0b00000110) >> 1;
|
||||||
|
uint8_t btr_idx = (data[2] & 0b11110000) >> 4;
|
||||||
|
uint8_t sr_idx = (data[2] & 0b00001100) >> 2;
|
||||||
|
uint8_t padded = (data[2] & 0b00000010) >> 1;
|
||||||
|
|
||||||
|
if ((version == kMpeg1V_INV) || (layer == kMpeg1L_INV) || (btr_idx == 0) ||
|
||||||
|
(sr_idx == 0b11))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
uint32_t bitrate = Mpeg1BitRate(btr_idx, version, layer);
|
||||||
|
uint32_t samplerate = Mpeg1SampleRate(sr_idx, version);
|
||||||
|
return Mpeg1FrameSize(layer, bitrate, samplerate, padded);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Mpeg1Header::GetAudioSpecificConfig(std::vector<uint8_t>* buffer) const {
|
||||||
|
// The following conversion table is extracted from ISO 14496 Part 3 -
|
||||||
|
// Table 1.16 - Sampling Frequency Index.
|
||||||
|
static const size_t kConfigFrequencyTable[] = {
|
||||||
|
96000, 88200, 64000, 48000, 44100, 32000, 24000,
|
||||||
|
22050, 16000, 12000, 11025, 8000, 7350};
|
||||||
|
static const size_t kConfigFrequencyTableSize =
|
||||||
|
arraysize(kConfigFrequencyTable);
|
||||||
|
uint8_t cft_idx;
|
||||||
|
|
||||||
|
for (cft_idx = 0; cft_idx < kConfigFrequencyTableSize; cft_idx++)
|
||||||
|
if (sample_rate_ == kConfigFrequencyTable[cft_idx])
|
||||||
|
break;
|
||||||
|
|
||||||
|
DCHECK(buffer);
|
||||||
|
buffer->clear();
|
||||||
|
BitWriter config(buffer);
|
||||||
|
config.WriteBits(GetObjectType(), 5);
|
||||||
|
config.WriteBits(cft_idx, 4);
|
||||||
|
/*
|
||||||
|
* NOTE: Number of channels matches channel_configuration index,
|
||||||
|
* since mpeg1 audio has only 1 or 2 channels
|
||||||
|
*/
|
||||||
|
config.WriteBits(GetNumChannels(), 4);
|
||||||
|
config.Flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t Mpeg1Header::GetObjectType() const {
|
||||||
|
/*
|
||||||
|
* ISO14496-3:2009 Table 1.17 - Audio Object Types
|
||||||
|
*/
|
||||||
|
if (layer_ == kMpeg1L_1)
|
||||||
|
return 32;
|
||||||
|
if (layer_ == kMpeg1L_2)
|
||||||
|
return 33;
|
||||||
|
|
||||||
|
DCHECK_EQ(layer_, kMpeg1L_3);
|
||||||
|
return 34;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t Mpeg1Header::GetSamplingFrequency() const {
|
||||||
|
return sample_rate_;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t Mpeg1Header::GetNumChannels() const {
|
||||||
|
if (channel_mode_ == 0b11)
|
||||||
|
return 1;
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace mp2t
|
||||||
|
} // namespace media
|
||||||
|
} // namespace shaka
|
|
@ -0,0 +1,56 @@
|
||||||
|
#ifndef PACKAGER_MEDIA_FORMATS_MP2T_MPEG1_HEADER_H_
|
||||||
|
#define PACKAGER_MEDIA_FORMATS_MP2T_MPEG1_HEADER_H_
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "packager/media/formats/mp2t/audio_header.h"
|
||||||
|
|
||||||
|
namespace shaka {
|
||||||
|
namespace media {
|
||||||
|
namespace mp2t {
|
||||||
|
/// Class which parses Mpeg1 audio frame (header / metadata) and synthesizes
|
||||||
|
/// AudioSpecificConfig from audio frame content.
|
||||||
|
///
|
||||||
|
/// See https://www.datavoyage.com/mpgscript/mpeghdr.htm
|
||||||
|
class Mpeg1Header : public AudioHeader {
|
||||||
|
public:
|
||||||
|
Mpeg1Header() = default;
|
||||||
|
~Mpeg1Header() override = default;
|
||||||
|
|
||||||
|
/// @name AudioHeader implementation overrides.
|
||||||
|
/// @{
|
||||||
|
bool IsSyncWord(const uint8_t* buf) const override;
|
||||||
|
size_t GetMinFrameSize() const override;
|
||||||
|
size_t GetSamplesPerFrame() const override;
|
||||||
|
bool Parse(const uint8_t* mpeg1_frame, size_t mpeg1_frame_size) override;
|
||||||
|
size_t GetHeaderSize() const override;
|
||||||
|
size_t GetFrameSize() const override;
|
||||||
|
size_t GetFrameSizeWithoutParsing(const uint8_t* data,
|
||||||
|
size_t num_bytes) const override;
|
||||||
|
void GetAudioSpecificConfig(std::vector<uint8_t>* buffer) const override;
|
||||||
|
uint8_t GetObjectType() const override;
|
||||||
|
uint32_t GetSamplingFrequency() const override;
|
||||||
|
uint8_t GetNumChannels() const override;
|
||||||
|
/// @}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Mpeg1Header(const Mpeg1Header&) = delete;
|
||||||
|
Mpeg1Header& operator=(const Mpeg1Header&) = delete;
|
||||||
|
|
||||||
|
uint8_t version_ = 0;
|
||||||
|
uint8_t layer_ = 0;
|
||||||
|
uint8_t protection_absent_ = 0;
|
||||||
|
|
||||||
|
uint32_t bitrate_ = 0;
|
||||||
|
uint32_t sample_rate_ = 0; /* in hz */
|
||||||
|
uint8_t padded_ = 0;
|
||||||
|
uint8_t channel_mode_ = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace mp2t
|
||||||
|
} // namespace media
|
||||||
|
} // namespace shaka
|
||||||
|
|
||||||
|
#endif // PACKAGER_MEDIA_FORMATS_MP2T_MPEG1_HEADER_H_
|
|
@ -0,0 +1,100 @@
|
||||||
|
#include <gmock/gmock.h>
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "packager/base/logging.h"
|
||||||
|
#include "packager/base/strings/string_number_conversions.h"
|
||||||
|
#include "packager/media/formats/mp2t/mpeg1_header.h"
|
||||||
|
|
||||||
|
using ::testing::ElementsAreArray;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
const char kValidMp3SyncByte[] = "FFFD800444333332114322";
|
||||||
|
|
||||||
|
const char kInvalidMp3SyncByte_1[] = "F00150802EFFFB80044433";
|
||||||
|
|
||||||
|
const char kInvalidMp3SyncByte_2[] = "FF8050802EDF";
|
||||||
|
|
||||||
|
const char kInvalidMp3SyncByte_3[] = "FF8050802EDFFF";
|
||||||
|
|
||||||
|
const char kValidMp3Frame[] = "ffFD800444333332114322";
|
||||||
|
|
||||||
|
const char kInvalidMp3FrameBadVersion[] = "FFE8800444333332114322";
|
||||||
|
|
||||||
|
const char kInvalidMp3FrameBadLayer[] = "FFF9800444333332114322";
|
||||||
|
|
||||||
|
const char kInvalidMp3FrameBadBitrate[] = "FFFD000444333332114322";
|
||||||
|
|
||||||
|
const char kInvalidMp3FrameBadSamepleRate[] = "FFFD8C0444333332114322";
|
||||||
|
|
||||||
|
} // anonymous namespace
|
||||||
|
|
||||||
|
namespace shaka {
|
||||||
|
namespace media {
|
||||||
|
namespace mp2t {
|
||||||
|
|
||||||
|
class Mpeg1HeaderTest : public testing::Test {
|
||||||
|
public:
|
||||||
|
void SetUp() override {
|
||||||
|
ASSERT_TRUE(base::HexStringToBytes(kValidMp3SyncByte, &sync_valid_));
|
||||||
|
ASSERT_TRUE(base::HexStringToBytes(kInvalidMp3SyncByte_1, &sync_inv_1_));
|
||||||
|
ASSERT_TRUE(base::HexStringToBytes(kInvalidMp3SyncByte_2, &sync_inv_2_));
|
||||||
|
ASSERT_TRUE(base::HexStringToBytes(kInvalidMp3SyncByte_3, &sync_inv_3_));
|
||||||
|
|
||||||
|
ASSERT_TRUE(base::HexStringToBytes(kValidMp3Frame, &frame_valid_));
|
||||||
|
ASSERT_TRUE(
|
||||||
|
base::HexStringToBytes(kInvalidMp3FrameBadVersion, &frame_inv_1_));
|
||||||
|
ASSERT_TRUE(
|
||||||
|
base::HexStringToBytes(kInvalidMp3FrameBadLayer, &frame_inv_2_));
|
||||||
|
ASSERT_TRUE(
|
||||||
|
base::HexStringToBytes(kInvalidMp3FrameBadBitrate, &frame_inv_3_));
|
||||||
|
ASSERT_TRUE(
|
||||||
|
base::HexStringToBytes(kInvalidMp3FrameBadSamepleRate, &frame_inv_4_));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::vector<uint8_t> sync_valid_;
|
||||||
|
std::vector<uint8_t> sync_inv_1_;
|
||||||
|
std::vector<uint8_t> sync_inv_2_;
|
||||||
|
std::vector<uint8_t> sync_inv_3_;
|
||||||
|
|
||||||
|
std::vector<uint8_t> frame_valid_;
|
||||||
|
std::vector<uint8_t> frame_inv_1_;
|
||||||
|
std::vector<uint8_t> frame_inv_2_;
|
||||||
|
std::vector<uint8_t> frame_inv_3_;
|
||||||
|
std::vector<uint8_t> frame_inv_4_;
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(Mpeg1HeaderTest, SyncBytes) {
|
||||||
|
Mpeg1Header mpeg1_header;
|
||||||
|
|
||||||
|
ASSERT_TRUE(mpeg1_header.IsSyncWord(sync_valid_.data()));
|
||||||
|
|
||||||
|
ASSERT_FALSE(mpeg1_header.IsSyncWord(sync_inv_1_.data()));
|
||||||
|
ASSERT_FALSE(mpeg1_header.IsSyncWord(sync_inv_2_.data()));
|
||||||
|
ASSERT_FALSE(mpeg1_header.IsSyncWord(sync_inv_3_.data()));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(Mpeg1HeaderTest, Parsing) {
|
||||||
|
Mpeg1Header mpeg1_header;
|
||||||
|
|
||||||
|
// Success parsing
|
||||||
|
EXPECT_EQ(static_cast<size_t>(417),
|
||||||
|
mpeg1_header.GetFrameSizeWithoutParsing(frame_valid_.data(),
|
||||||
|
frame_valid_.size()));
|
||||||
|
EXPECT_TRUE(mpeg1_header.Parse(frame_valid_.data(), frame_valid_.size()));
|
||||||
|
EXPECT_EQ(static_cast<size_t>(417), mpeg1_header.GetFrameSize());
|
||||||
|
EXPECT_EQ(static_cast<size_t>(44100), mpeg1_header.GetSamplingFrequency());
|
||||||
|
EXPECT_EQ(static_cast<size_t>(1152), mpeg1_header.GetSamplesPerFrame());
|
||||||
|
EXPECT_EQ(2, mpeg1_header.GetNumChannels());
|
||||||
|
|
||||||
|
// Failed parsing
|
||||||
|
EXPECT_FALSE(mpeg1_header.Parse(frame_inv_1_.data(), frame_inv_1_.size()));
|
||||||
|
EXPECT_FALSE(mpeg1_header.Parse(frame_inv_2_.data(), frame_inv_2_.size()));
|
||||||
|
EXPECT_FALSE(mpeg1_header.Parse(frame_inv_3_.data(), frame_inv_3_.size()));
|
||||||
|
EXPECT_FALSE(mpeg1_header.Parse(frame_inv_4_.data(), frame_inv_4_.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // Namespace mp2t
|
||||||
|
} // namespace media
|
||||||
|
} // namespace shaka
|
|
@ -63,9 +63,10 @@ bool PesPacketGenerator::Initialize(const StreamInfo& stream_info) {
|
||||||
return adts_converter_->Parse(audio_stream_info.codec_config());
|
return adts_converter_->Parse(audio_stream_info.codec_config());
|
||||||
}
|
}
|
||||||
if (audio_stream_info.codec() == Codec::kCodecAC3 ||
|
if (audio_stream_info.codec() == Codec::kCodecAC3 ||
|
||||||
audio_stream_info.codec() == Codec::kCodecEAC3) {
|
audio_stream_info.codec() == Codec::kCodecEAC3 ||
|
||||||
|
audio_stream_info.codec() == Codec::kCodecMP3) {
|
||||||
audio_stream_id_ = kAc3AudioStreamId;
|
audio_stream_id_ = kAc3AudioStreamId;
|
||||||
// No converter needed for AC3 and E-AC3.
|
// No converter needed for AC3, E-AC3 and MP3.
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
NOTIMPLEMENTED() << "Audio codec " << audio_stream_info.codec()
|
NOTIMPLEMENTED() << "Audio codec " << audio_stream_info.codec()
|
||||||
|
|
|
@ -261,6 +261,9 @@ bool ProgramMapTableWriter::ClearSegmentPmt(BufferWriter* writer) {
|
||||||
case kCodecAAC:
|
case kCodecAAC:
|
||||||
stream_type = TsStreamType::kAdtsAac;
|
stream_type = TsStreamType::kAdtsAac;
|
||||||
break;
|
break;
|
||||||
|
case kCodecMP3:
|
||||||
|
stream_type = TsStreamType::kMpeg1Audio;
|
||||||
|
break;
|
||||||
case kCodecAC3:
|
case kCodecAC3:
|
||||||
stream_type = TsStreamType::kAc3;
|
stream_type = TsStreamType::kAc3;
|
||||||
break;
|
break;
|
||||||
|
@ -314,6 +317,9 @@ bool AudioProgramMapTableWriter::WriteDescriptors(
|
||||||
case kCodecAAC:
|
case kCodecAAC:
|
||||||
fourcc = FOURCC_aacd;
|
fourcc = FOURCC_aacd;
|
||||||
break;
|
break;
|
||||||
|
case kCodecMP3:
|
||||||
|
fourcc = FOURCC_mp3a;
|
||||||
|
break;
|
||||||
case kCodecAC3:
|
case kCodecAC3:
|
||||||
fourcc = FOURCC_ac3d;
|
fourcc = FOURCC_ac3d;
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -230,7 +230,8 @@ Status ValidateStreamDescriptor(bool dump_stream_info,
|
||||||
"descriptors 'output' or 'init_segment' are not allowed.");
|
"descriptors 'output' or 'init_segment' are not allowed.");
|
||||||
}
|
}
|
||||||
} else if (output_format == CONTAINER_WEBVTT ||
|
} else if (output_format == CONTAINER_WEBVTT ||
|
||||||
output_format == CONTAINER_AAC || output_format == CONTAINER_AC3 ||
|
output_format == CONTAINER_AAC || output_format == CONTAINER_MP3 ||
|
||||||
|
output_format == CONTAINER_AC3 ||
|
||||||
output_format == CONTAINER_EAC3) {
|
output_format == CONTAINER_EAC3) {
|
||||||
// There is no need for an init segment when outputting because there is no
|
// There is no need for an init segment when outputting because there is no
|
||||||
// initialization data.
|
// initialization data.
|
||||||
|
@ -807,6 +808,7 @@ Status CreateAllJobs(const std::vector<StreamDescriptor>& stream_descriptors,
|
||||||
switch (GetOutputFormat(stream)) {
|
switch (GetOutputFormat(stream)) {
|
||||||
case CONTAINER_MPEG2TS:
|
case CONTAINER_MPEG2TS:
|
||||||
case CONTAINER_AAC:
|
case CONTAINER_AAC:
|
||||||
|
case CONTAINER_MP3:
|
||||||
case CONTAINER_AC3:
|
case CONTAINER_AC3:
|
||||||
case CONTAINER_EAC3:
|
case CONTAINER_EAC3:
|
||||||
has_transport_audio_video_streams = true;
|
has_transport_audio_video_streams = true;
|
||||||
|
|
Loading…
Reference in New Issue