Merge branch 'refs/heads/devel'

This commit is contained in:
Martchus 2016-02-20 03:05:17 +01:00
commit c473f6ad7e
40 changed files with 1299 additions and 290 deletions

1
.gitignore vendored
View File

@ -15,6 +15,7 @@
/.qmake.cache
/.qmake.stash
*.pro.user
*.txt.user
*.pro.user.*
*.qbs.user
*.qbs.user.*

View File

@ -18,8 +18,9 @@ set(HEADER_FILES
abstracttrack.h
adts/adtsframe.h
adts/adtsstream.h
# avc/avcconfiguration.h
# avc/avcinfo.h
aspectratio.h
avc/avcconfiguration.h
avc/avcinfo.h
avi/bitmapinfoheader.h
backuphelper.h
basicfileinfo.h
@ -32,6 +33,7 @@ set(HEADER_FILES
ogg/oggiterator.h
ogg/oggpage.h
ogg/oggstream.h
opus/opusidentificationheader.h
positioninset.h
signature.h
size.h
@ -88,8 +90,9 @@ set(SRC_FILES
abstracttrack.cpp
adts/adtsframe.cpp
adts/adtsstream.cpp
# avc/avcconfiguration.cpp
# avc/avcinfo.cpp
aspectratio.cpp
avc/avcconfiguration.cpp
avc/avcinfo.cpp
avi/bitmapinfoheader.cpp
backuphelper.cpp
basicfileinfo.cpp
@ -101,6 +104,7 @@ set(SRC_FILES
ogg/oggiterator.cpp
ogg/oggpage.cpp
ogg/oggstream.cpp
opus/opusidentificationheader.cpp
signature.cpp
statusprovider.cpp
tag.cpp
@ -132,6 +136,13 @@ set(SRC_FILES
mediafileinfo.cpp
mediaformat.cpp
)
set(TEST_HEADER_FILES
)
set(TEST_SRC_FILES
tests/cppunit.cpp
tests/matroska.cpp
)
# meta data
set(META_PROJECT_NAME tagparser)
@ -139,9 +150,9 @@ set(META_APP_NAME "Tag Parser")
set(META_APP_AUTHOR "Martchus")
set(META_APP_URL "https://github.com/${META_APP_AUTHOR}/${META_PROJECT_NAME}")
set(META_APP_DESCRIPTION "C++ library for reading and writing MP4 (iTunes), ID3, Vorbis and Matroska tags.")
set(META_VERSION_MAJOR 4)
set(META_VERSION_MAJOR 5)
set(META_VERSION_MINOR 0)
set(META_VERSION_PATCH 2)
set(META_VERSION_PATCH 0)
# stringification of meta data
set(META_PROJECT_NAME_STR "\"${META_PROJECT_NAME}\"")
@ -187,6 +198,18 @@ add_definitions(
-D_GLIBCXX_USE_CXX11_ABI=0
)
# add check target
if(NOT TARGET check)
set(CMAKE_CTEST_COMMAND ctest -V)
add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND})
enable_testing()
endif()
add_executable(${META_PROJECT_NAME}_tests EXCLUDE_FROM_ALL ${TEST_HEADER_FILES} ${TEST_SRC_FILES})
target_link_libraries(${META_PROJECT_NAME}_tests ${META_PROJECT_NAME} c++utilities cppunit)
set_target_properties(${META_PROJECT_NAME}_tests PROPERTIES CXX_STANDARD 11)
add_test(NAME ${META_PROJECT_NAME}_cppunit COMMAND ${META_PROJECT_NAME}_tests -p "${CMAKE_CURRENT_SOURCE_DIR}/testfiles")
add_dependencies(check ${META_PROJECT_NAME}_tests)
# executable and linking
add_library(${META_PROJECT_NAME} SHARED ${HEADER_FILES} ${SRC_FILES} ${RES_FILES} ${WINDOWS_ICON_PATH})
target_link_libraries(${META_PROJECT_NAME} c++utilities z)
@ -213,21 +236,31 @@ foreach(HEADER_FILE ${HEADER_FILES})
COMPONENT header
)
endforeach()
add_custom_target(install-binary
DEPENDS ${META_PROJECT_NAME}
COMMAND "${CMAKE_COMMAND}" -DCMAKE_INSTALL_COMPONENT=binary -P "${CMAKE_BINARY_DIR}/cmake_install.cmake"
)
add_custom_target(install-header
DEPENDS ${META_PROJECT_NAME}
COMMAND "${CMAKE_COMMAND}" -DCMAKE_INSTALL_COMPONENT=header -P "${CMAKE_BINARY_DIR}/cmake_install.cmake"
)
add_custom_target(install-mingw-w64
DEPENDS install-binary install-header
)
add_custom_target(install-binary-strip
DEPENDS ${META_PROJECT_NAME}
COMMAND "${CMAKE_COMMAND}" -DCMAKE_INSTALL_DO_STRIP=1 -DCMAKE_INSTALL_COMPONENT=binary -P "${CMAKE_BINARY_DIR}/cmake_install.cmake"
)
add_custom_target(install-mingw-w64-strip
DEPENDS install-binary-strip install-header
)
if(NOT TARGET install-binary)
add_custom_target(install-binary
DEPENDS ${META_PROJECT_NAME}
COMMAND "${CMAKE_COMMAND}" -DCMAKE_INSTALL_COMPONENT=binary -P "${CMAKE_BINARY_DIR}/cmake_install.cmake"
)
endif()
if(NOT TARGET install-header)
add_custom_target(install-header
DEPENDS ${META_PROJECT_NAME}
COMMAND "${CMAKE_COMMAND}" -DCMAKE_INSTALL_COMPONENT=header -P "${CMAKE_BINARY_DIR}/cmake_install.cmake"
)
endif()
if(NOT TARGET install-mingw-w64)
add_custom_target(install-mingw-w64
DEPENDS install-binary install-header
)
endif()
if(NOT TARGET install-binary-strip)
add_custom_target(install-binary-strip
DEPENDS ${META_PROJECT_NAME}
COMMAND "${CMAKE_COMMAND}" -DCMAKE_INSTALL_DO_STRIP=1 -DCMAKE_INSTALL_COMPONENT=binary -P "${CMAKE_BINARY_DIR}/cmake_install.cmake"
)
endif()
if(NOT TARGET install-mingw-w64-strip)
add_custom_target(install-mingw-w64-strip
DEPENDS install-binary-strip install-header
)
endif()

View File

@ -5,7 +5,7 @@ C++ library for reading and writing MP4 (iTunes), ID3, Vorbis and Matroska tags.
The tag library can read and write the following tag formats:
- iTunes-style MP4 tags (MP4-DASH is supported)
- ID3v1 and ID3v2 tags
- Vorbis comments (cover art via "METADATA_BLOCK_PICTURE" is supported)
- Vorbis and Opus comments (cover art via "METADATA_BLOCK_PICTURE" is supported) in Ogg streams
- Matroska/WebM tags and attachments
## File layout options
@ -21,6 +21,8 @@ or appending the tag. Usage of padding can be configured:
However, it is also possible to force rewriting the entire file.
Taking advantage of padding is currently not supported when dealing with Ogg streams.
## Additional features
The library can also display technical information such as the ID, format, language, bitrate,
duration, size, timestamps, sampling frequency, FPS and other information of the tracks.

View File

@ -15,6 +15,11 @@
using namespace std;
using namespace IoUtilities;
/*!
* \remarks Nothing of this code has been tested yet. These classes are
* not used by the rest of the library (currently).
*/
namespace Media {
/*!

View File

@ -55,6 +55,7 @@ AbstractTrack::AbstractTrack(istream &inputStream, ostream &outputStream, uint64
m_quality(0),
m_depth(0),
m_fps(0),
m_chromaFormat(nullptr),
m_interlaced(false),
m_timeScale(0),
m_enabled(true),

View File

@ -4,6 +4,7 @@
#include "./statusprovider.h"
#include "./size.h"
#include "./margin.h"
#include "./aspectratio.h"
#include "./mediaformat.h"
#include <c++utilities/conversion/types.h>
@ -21,6 +22,7 @@ enum class MediaType;
enum class GeneralMediaFormat;
class MpegAudioFrameStream;
class WaveAudioStream;
class Mp4Track;
/*!
* \brief Specifies the track type.
@ -40,6 +42,7 @@ class LIB_EXPORT AbstractTrack : public StatusProvider
{
friend class MpegAudioFrameStream;
friend class WaveAudioStream;
friend class Mp4Track;
public:
virtual ~AbstractTrack();
@ -85,6 +88,8 @@ public:
const std::string &compressorName() const;
uint16 depth() const;
uint32 fps() const;
const char *chromaFormat() const;
const AspectRatio &pixelAspectRatio() const;
bool isInterlaced() const;
uint32 timeScale() const;
bool isEnabled() const;
@ -141,6 +146,8 @@ protected:
std::string m_compressorName;
uint16 m_depth;
uint32 m_fps;
const char *m_chromaFormat;
AspectRatio m_pixelAspectRatio;
bool m_interlaced;
uint32 m_timeScale;
bool m_enabled;
@ -241,7 +248,7 @@ inline MediaFormat AbstractTrack::format() const
}
/*!
* \brief Returns the version of the track if known; otherwise returns 0.
* \brief Returns the version/level of the track if known; otherwise returns 0.
*/
inline double AbstractTrack::version() const
{
@ -489,6 +496,24 @@ inline uint32 AbstractTrack::fps() const
return m_fps;
}
/*!
* \brief Returns the chroma subsampling format if known; otherwise returns nullptr.
*
* This value only makes sense for video tracks.
*/
inline const char *AbstractTrack::chromaFormat() const
{
return m_chromaFormat;
}
/*!
* \brief Returns the pixel aspect ratio (PAR).
*/
inline const AspectRatio &AbstractTrack::pixelAspectRatio() const
{
return m_pixelAspectRatio;
}
/*!
* \brief Returns true if the video is denoted as interlaced; otherwise returns false.
*

32
aspectratio.cpp Normal file
View File

@ -0,0 +1,32 @@
#include "./aspectratio.h"
using namespace std;
namespace Media {
/*!
* \struct Media::AspectRatio
* \brief The AspectRatio struct defines an aspect ratio.
*/
/*!
* \brief Constructs a PAR form the specified AVC aspectRatioType.
*/
AspectRatio::AspectRatio(byte aspectRatioType)
{
static const AspectRatio predefinedPars[] = {
AspectRatio(), AspectRatio(1, 1), AspectRatio(12, 11), AspectRatio(10, 11),
AspectRatio(16, 11), AspectRatio(40, 33), AspectRatio(24, 11), AspectRatio(20, 11),
AspectRatio(32, 11), AspectRatio(80, 33), AspectRatio(18, 11), AspectRatio(15, 11),
AspectRatio(64, 33), AspectRatio(160, 99), AspectRatio(4, 3), AspectRatio(3, 2),
AspectRatio(2, 1)
};
if(aspectRatioType < sizeof(predefinedPars)) {
*this = predefinedPars[aspectRatioType];
} else {
numerator = denominator = 0;
}
type = aspectRatioType;
}
}

57
aspectratio.h Normal file
View File

@ -0,0 +1,57 @@
#ifndef MEDIA_ASPECTRATIO_H
#define MEDIA_ASPECTRATIO_H
#include <c++utilities/application/global.h>
#include <c++utilities/conversion/types.h>
namespace Media {
struct LIB_EXPORT AspectRatio {
AspectRatio();
AspectRatio(byte aspectRatioType);
AspectRatio(uint16 numerator, uint16 denominator);
bool isValid() const;
bool isExtended() const;
byte type;
uint16 numerator;
uint16 denominator;
};
/*!
* \brief Constructs an invalid aspect ratio.
*/
inline AspectRatio::AspectRatio() :
type(0),
numerator(0),
denominator(0)
{}
/*!
* \brief Constructs a aspect ratio with the specified \a numerator and \a denominator.
*/
inline AspectRatio::AspectRatio(uint16 numerator, uint16 denominator) :
type(0xFF),
numerator(numerator),
denominator(denominator)
{}
/*!
* \brief Returns an indication whether the aspect ratio is present and valid.
*/
inline bool AspectRatio::isValid() const
{
return !type || !numerator || !denominator;
}
/*!
* \brief Returns whether numerator and denominator must be read from extended SAR header.
*/
inline bool AspectRatio::isExtended() const
{
return type == 0xFF;
}
}
#endif // MEDIA_ASPECTRATIO_H

View File

@ -1,5 +1,75 @@
#include "./avcconfiguration.h"
#include "../exceptions.h"
#include "../mediaformat.h"
#include <c++utilities/io/binaryreader.h>
using namespace std;
using namespace IoUtilities;
namespace Media {
void AvcConfiguration::parse(BinaryReader &reader, uint64 maxSize)
{
if(maxSize < 7) {
throw TruncatedDataException();
}
maxSize -= 7;
reader.stream()->seekg(1, ios_base::cur); // always 1
profileIndication = reader.readByte();
profileCompat = reader.readByte();
levelIndication = reader.readByte();
naluSizeLength = (reader.readByte() & 0x03) + 1;
// read SPS info entries
byte entryCount = reader.readByte() & 0x0f;
spsInfos.reserve(entryCount);
for(; entryCount; --entryCount) {
if(maxSize < 2) {
throw TruncatedDataException();
}
spsInfos.emplace_back();
try {
spsInfos.back().parse(reader, maxSize);
} catch(const TruncatedDataException &) {
// TODO: log parsing error
if(spsInfos.back().size > maxSize - 2) {
throw;
}
spsInfos.pop_back();
} catch(const Failure &) {
spsInfos.pop_back();
// TODO: log parsing error
}
maxSize -= spsInfos.back().size;
}
// read PPS info entries
entryCount = reader.readByte();
ppsInfos.reserve(entryCount);
for(; entryCount; --entryCount) {
if(maxSize < 2) {
throw TruncatedDataException();
}
ppsInfos.emplace_back();
try {
ppsInfos.back().parse(reader, maxSize);
} catch(const TruncatedDataException &) {
// TODO: log parsing error
if(ppsInfos.back().size > maxSize - 2) {
throw;
}
ppsInfos.pop_back();
} catch(const Failure &) {
ppsInfos.pop_back();
// TODO: log parsing error
}
maxSize -= ppsInfos.back().size;
}
// ignore remaining data
}
}

View File

@ -7,21 +7,25 @@
namespace Media {
class MediaFormat;
struct LIB_EXPORT AvcConfiguration
{
AvcConfiguration();
byte profileIdc;
byte profileIndication;
byte profileCompat;
byte levelIdc;
byte levelIndication;
byte naluSizeLength;
std::vector<SpsInfo> spsInfos;
std::vector<PpsInfo> ppsInfos;
void parse(IoUtilities::BinaryReader &reader, uint64 maxSize);
};
inline AvcConfiguration::AvcConfiguration() :
profileIdc(0),
profileIndication(0),
profileCompat(0),
levelIdc(0),
levelIndication(0),
naluSizeLength(0)
{}

View File

@ -1,7 +1,16 @@
#include "./avcinfo.h"
#include "../exceptions.h"
#include <c++utilities/io/binaryreader.h>
#include <c++utilities/io/bitreader.h>
#include <c++utilities/misc/memory.h>
#include <unordered_map>
using namespace std;
using namespace IoUtilities;
namespace Media {
/*!
@ -10,25 +19,255 @@ namespace Media {
*/
/*!
* \brief SpsInfo::parse
* \param stream
* \brief Parses the SPS info.
*/
void SpsInfo::parse(std::istream &stream)
void SpsInfo::parse(BinaryReader &reader, uint32 maxSize)
{
static auto highLevelProfileIds = std::unordered_map<unsigned int, bool> {
{ 44, true }, { 83, true }, { 86, true }, { 100, true }, { 110, true },
{ 118, true }, { 122, true }, { 128, true }, { 244, true }
};
int const addSpace = 100;
// read (and check) size
if(maxSize < 2) {
throw TruncatedDataException();
}
maxSize -= 2;
if((size = reader.readUInt16BE()) > maxSize) {
throw TruncatedDataException();
}
parNum = 1;
parDen = 1;
arFound = false;
// buffer data for reading with BitReader
auto buffer = make_unique<char[]>(size);
reader.read(buffer.get(), size);
BitReader bitReader(buffer.get(), size);
uint32 unitsInTick = 0;
uint32 timeScale = 0;
try {
// read general values
bitReader.skipBits(3);
if(bitReader.readBits<byte>(5) != 7) {
throw InvalidDataException();
}
profileIndication = bitReader.readBits<byte>(8);
profileConstraints = bitReader.readBits<byte>(8);
levelIndication = bitReader.readBits<byte>(8);
id = bitReader.readUnsignedExpGolombCodedBits<ugolomb>();
// read chroma profile specific values
switch(profileIndication) {
case 44: case 83: case 86: case 100: case 110:
case 118: case 122: case 128: case 244:
// high-level profile
if((chromaFormatIndication = bitReader.readUnsignedExpGolombCodedBits<ugolomb>()) == 3) {
bitReader.skipBits(1); // separate color plane flag
}
bitReader.readUnsignedExpGolombCodedBits<byte>(); // bit depth luma minus8
bitReader.readUnsignedExpGolombCodedBits<byte>(); // bit depth chroma minus8
bitReader.skipBits(1); // qpprime y zero transform bypass flag
if(bitReader.readBit()) { // sequence scaling matrix present flag
for(byte i = 0; i < 8; ++i) {
// TODO: store values
if(bitReader.readBit()) { // sequence scaling list present
if(i < 6) {
bitReader.skipBits(16); // scalingList4x4[i]
} else {
bitReader.skipBits(64); // scalingList8x8[i - 6]
}
}
}
}
break;
default:
chromaFormatIndication = 1; // assume YUV 4:2:0
}
// read misc values
log2MaxFrameNum = bitReader.readUnsignedExpGolombCodedBits<ugolomb>() + 4;
switch(pictureOrderCountType = bitReader.readUnsignedExpGolombCodedBits<ugolomb>()) {
case 0:
log2MaxPictureOrderCountLsb = bitReader.readUnsignedExpGolombCodedBits<ugolomb>() + 4;
break;
case 1:
deltaPicOrderAlwaysZeroFlag = bitReader.readBit();
offsetForNonRefPic = bitReader.readSignedExpGolombCodedBits<sgolomb>();
offsetForTopToBottomField = bitReader.readSignedExpGolombCodedBits<sgolomb>();
numRefFramesInPicOrderCntCycle = bitReader.readUnsignedExpGolombCodedBits<ugolomb>();
for(byte i = 0; i < numRefFramesInPicOrderCntCycle; ++i) {
bitReader.readUnsignedExpGolombCodedBits<ugolomb>(); // offset for ref frames
}
break;
case 2:
break;
default:
throw InvalidDataException();
}
bitReader.readUnsignedExpGolombCodedBits<ugolomb>(); // ref frames num
bitReader.skipBits(1); // gaps in frame num value allowed flag
// read picture size related values
Size mbSize;
mbSize.setWidth(bitReader.readUnsignedExpGolombCodedBits<uint32>() + 1);
mbSize.setHeight(bitReader.readUnsignedExpGolombCodedBits<uint32>() + 1);
if(!(frameMbsOnly = bitReader.readBit())) { // frame mbs only flag
bitReader.readBit(); // mb adaptive frame field flag
}
bitReader.skipBits(1); // distinct 8x8 inference flag
// read cropping values
if(bitReader.readBit()) { // frame cropping flag
cropping.setLeft(bitReader.readUnsignedExpGolombCodedBits<uint32>());
cropping.setRight(bitReader.readUnsignedExpGolombCodedBits<uint32>());
cropping.setTop(bitReader.readUnsignedExpGolombCodedBits<uint32>());
cropping.setBottom(bitReader.readUnsignedExpGolombCodedBits<uint32>());
}
// read VUI (video usability information)
if((vuiPresent = bitReader.readBit())) {
if((bitReader.readBit())) { // PAR present flag
pixelAspectRatio = AspectRatio(bitReader.readBits<byte>(8));
if(pixelAspectRatio.isExtended()) {
// read extended SAR
pixelAspectRatio.numerator = bitReader.readBits<uint16>(16);
pixelAspectRatio.denominator = bitReader.readBits<uint16>(16);
}
}
// read/skip misc values
if(bitReader.readBit()) { // overscan info present
bitReader.skipBits(1); // overscan appropriate
}
if(bitReader.readBit()) { // video signal type present
bitReader.skipBits(4); // video format and video full range
if(bitReader.readBit()) { // color description present
bitReader.skipBits(24); // color primaries, transfer characteristics, matrix coefficients
}
}
if(bitReader.readBit()) { // chroma loc info present
bitReader.readUnsignedExpGolombCodedBits<byte>(); // chroma sample loc type top field
bitReader.readUnsignedExpGolombCodedBits<byte>(); // chroma sample loc type bottom field
}
// read timing info
if((timingInfo.isPresent = bitReader.readBit())) {
timingInfo.unitsInTick = bitReader.readBits<uint32>(32);
timingInfo.timeScale = bitReader.readBits<uint32>(32);
timingInfo.fixedFrameRate = bitReader.readBit();
}
// skip hrd parameters
hrdParametersPresent = 0;
if(bitReader.readBit()) { // nal hrd parameters present
nalHrdParameters.parse(bitReader);
hrdParametersPresent = 1;
}
if(bitReader.readBit()) { // vcl hrd parameters present
vclHrdParameters.parse(bitReader);
hrdParametersPresent = 1;
}
if(hrdParametersPresent) {
bitReader.skipBits(1); // low delay hrd flag
}
pictureStructPresent = bitReader.readBit();
if(bitReader.readBit()) { // bitstream restriction flag
bitReader.skipBits(1); // motion vectors over pic boundries flag
bitReader.readUnsignedExpGolombCodedBits<byte>(); // max bytes per pic denom
bitReader.readUnsignedExpGolombCodedBits<byte>(); // max bytes per mb denom
bitReader.readUnsignedExpGolombCodedBits<byte>(); // log2 max mv length horizontal
bitReader.readUnsignedExpGolombCodedBits<byte>(); // log2 max mv length vertical
bitReader.readUnsignedExpGolombCodedBits<byte>(); // reorder frames num
bitReader.readUnsignedExpGolombCodedBits<byte>(); // max decoder frame buffering
}
}
// calculate actual picture size
if(!cropping.isNull()) {
// determine cropping scale
ugolomb croppingScaleX, croppingScaleY;
switch(chromaFormatIndication) {
case 1: // 4:2:0
croppingScaleX = 2;
croppingScaleY = frameMbsOnly ? 2 : 4;
break;
case 2: // 4:2:2
croppingScaleX = 2;
croppingScaleY = 2 - frameMbsOnly;
break;
default: // case 0: monochrome, case 3: 4:4:4
croppingScaleX = 1;
croppingScaleY = 2 - frameMbsOnly;
break;
}
pictureSize.setWidth(mbSize.width() * 16 - croppingScaleX * (cropping.left() + cropping.right()));
pictureSize.setHeight((2 - frameMbsOnly) * mbSize.height() * 16 - croppingScaleY * (cropping.top() + cropping.bottom()));
} else {
pictureSize.setWidth(mbSize.width() * 16);
pictureSize.setHeight((2 - frameMbsOnly) * mbSize.height() * 16);
}
} catch(const ios_base::failure &) {
throw TruncatedDataException();
}
}
/*!
* \struct Media::PpsInfo
* \brief The PpsInfo struct holds the picture parameter set.
*/
/*!
* \brief Parses the PPS info.
*/
void PpsInfo::parse(BinaryReader &reader, uint32 maxSize)
{
// read (and check) size
if(maxSize < 2) {
throw TruncatedDataException();
}
maxSize -= 2;
if((size = reader.readUInt16BE()) > maxSize) {
throw TruncatedDataException();
}
// buffer data for reading with BitReader
auto buffer = make_unique<char[]>(size);
reader.read(buffer.get(), size);
BitReader bitReader(buffer.get(), size);
try {
// read general values
bitReader.skipBits(1); // zero bit
if(bitReader.readBits<byte>(5) != 8) { // nal unit type
throw InvalidDataException();
}
id = bitReader.readUnsignedExpGolombCodedBits<ugolomb>();
spsId = bitReader.readUnsignedExpGolombCodedBits<ugolomb>();
bitReader.skipBits(1); // entropy coding mode flag
picOrderPresent = bitReader.readBit();
} catch(const ios_base::failure &) {
throw TruncatedDataException();
}
}
/*!
* \struct Media::HrdParameters
* \brief The HrdParameters struct holds "Hypothetical Reference Decoder" parameters.
* \remarks This is "a model for thinking about the decoding process".
*/
/*!
* \brief Parses HRD parameters.
*/
void HrdParameters::parse(IoUtilities::BitReader &reader)
{
cpbCount = reader.readUnsignedExpGolombCodedBits<ugolomb>() + 1;
bitRateScale = reader.readBits<byte>(4);
cpbSizeScale = reader.readBits<byte>(4);
for(ugolomb i = 0; i < cpbCount; ++i) {
// just skip those values
reader.readUnsignedExpGolombCodedBits<byte>(); // bit rate value minus 1
reader.readUnsignedExpGolombCodedBits<byte>(); // cpb size value minus 1
reader.skipBits(1); // cbr flag
}
initialCpbRemovalDelayLength = reader.readBits<byte>(5) + 1;
cpbRemovalDelayLength = reader.readBits<byte>(5) + 1;
cpbOutputDelayLength = reader.readBits<byte>(5) + 1;
timeOffsetLength = reader.readBits<byte>(5);
}

View File

@ -3,23 +3,39 @@
#include "../margin.h"
#include "../size.h"
#include "../aspectratio.h"
namespace IoUtilities {
class BinaryReader;
class BitReader;
}
namespace Media {
/*!
* \brief Type used to store unsigned integer values using golomb coding.
*/
typedef uint32 ugolomb;
/*!
* \brief Type used to store signed integer values using golomb coding.
*/
typedef int32 sgolomb;
struct LIB_EXPORT TimingInfo {
TimingInfo();
uint32 unitsInTick;
uint32 timeScale;
bool isPresent;
bool fixedFrameRate;
byte isPresent;
byte fixedFrameRate;
int64 defaultDuration() const;
};
inline TimingInfo::TimingInfo() :
unitsInTick(0),
timeScale(0),
isPresent(false),
fixedFrameRate(false)
isPresent(0),
fixedFrameRate(0)
{}
inline int64 TimingInfo::defaultDuration() const
@ -27,61 +43,93 @@ inline int64 TimingInfo::defaultDuration() const
return 1000000000ll * unitsInTick / timeScale;
}
struct LIB_EXPORT HrdParameters {
HrdParameters();
ugolomb cpbCount;
byte bitRateScale;
byte cpbSizeScale;
byte initialCpbRemovalDelayLength;
byte cpbRemovalDelayLength;
byte cpbOutputDelayLength;
byte timeOffsetLength;
void parse(IoUtilities::BitReader &reader);
};
inline HrdParameters::HrdParameters() :
cpbCount(0),
bitRateScale(0),
cpbSizeScale(0),
initialCpbRemovalDelayLength(0),
cpbRemovalDelayLength(0),
cpbOutputDelayLength(0),
timeOffsetLength(0)
{}
struct LIB_EXPORT SpsInfo {
SpsInfo();
uint32 id;
uint32 profileIndication;
uint32 profileCompat;
uint32 levelIdc;
uint32 chromatFormatIndication;
uint32 log2MaxFrameNum;
uint32 offsetForNonRefPic;
uint32 offsetForTopToBottomField;
uint32 numRefFramesInPicOrderCntCycle;
bool deltaPicOrderAlwaysZeroFlag;
bool frameMbsOnly;
bool vuiPresent;
bool arFound;
uint32 parNum;
uint32 parDen;
ugolomb id;
byte profileIndication;
byte profileConstraints;
byte levelIndication;
ugolomb chromaFormatIndication;
ugolomb pictureOrderCountType;
ugolomb log2MaxFrameNum;
ugolomb log2MaxPictureOrderCountLsb;
sgolomb offsetForNonRefPic;
sgolomb offsetForTopToBottomField;
ugolomb numRefFramesInPicOrderCntCycle;
byte deltaPicOrderAlwaysZeroFlag;
byte frameMbsOnly;
byte vuiPresent;
AspectRatio pixelAspectRatio;
TimingInfo timingInfo;
Margin cropping;
Size size;
uint32 checksum;
Size pictureSize;
byte hrdParametersPresent;
HrdParameters nalHrdParameters;
HrdParameters vclHrdParameters;
byte pictureStructPresent;
uint16 size;
void parse(std::istream &stream);
void parse(IoUtilities::BinaryReader &reader, uint32 maxSize);
};
inline SpsInfo::SpsInfo() :
id(0),
profileIndication(0),
profileCompat(0),
levelIdc(0),
chromatFormatIndication(0),
profileConstraints(0),
levelIndication(0),
chromaFormatIndication(0),
pictureOrderCountType(0),
log2MaxFrameNum(0),
log2MaxPictureOrderCountLsb(0),
offsetForNonRefPic(0),
offsetForTopToBottomField(0),
numRefFramesInPicOrderCntCycle(0),
deltaPicOrderAlwaysZeroFlag(false),
frameMbsOnly(false),
vuiPresent(false),
arFound(false),
parNum(0),
parDen(0),
checksum(0)
deltaPicOrderAlwaysZeroFlag(0),
frameMbsOnly(0),
vuiPresent(0),
hrdParametersPresent(0),
pictureStructPresent(0),
size(0)
{}
struct PpsInfo {
PpsInfo();
uint32 id;
uint32 spsId;
bool picOrderPresent;
ugolomb id;
ugolomb spsId;
byte picOrderPresent;
uint16 size;
void parse(IoUtilities::BinaryReader &reader, uint32 maxSize);
};
inline PpsInfo::PpsInfo() :
id(0),
spsId(0),
picOrderPresent(false)
picOrderPresent(false),
size(0)
{}
struct SliceInfo {
@ -99,7 +147,7 @@ struct SliceInfo {
uint32 deltaPicOrderCnt[2];
uint32 firstMbInSlice;
uint32 sps;
uint32 pps;
uint32 pps;
};
inline SliceInfo::SliceInfo() :

77
download_testfiles.sh Executable file
View File

@ -0,0 +1,77 @@
#!/bin/sh
testfilespath="$1"
# determine sequences for formatted output
red=$(tput setaf 1)
green=$(tput setaf 2)
yellow=$(tput setaf 3)
blue=$(tput setaf 4)
bold=$(tput bold)
normal=$(tput sgr0)
inform() {
echo "${bold}==> ${blue}INFO:${normal} ${bold}${1}${normal}"
}
success() {
echo "${bold}==> ${green}SUCCESS:${normal} ${bold}${1}${normal}"
}
fail() {
echo "${bold}==> ${red}FAILURE:${normal} ${bold}${1}${normal}"
}
skipping() {
echo 'archive/files already exist -> skipping download'
}
download() {
title="$1" url="$2" filename="$3" destdir="$4"
inform "Downloading \"$title\" ..."
if [[ ! -d $destdir ]]; then
# actual download
pushd '/tmp'
if [[ ! -f $filename ]]; then
wget --output-document="$filename" "$url"
if [[ $? != 0 ]]; then
fail "unable to download: \"$url\""
return
fi
else
skipping
fi
popd
# extraction
mkdir "$destdir"
pushd "$destdir"
case "$filename" in
*.zip) unzip "/tmp/$filename";;
*) fail "unable to extract archive: format \"${filename#*.}\" not supported"
return;;
esac
if [[ $? != 0 ]]; then
fail "unable to extract \"/tmp/$filename\""
return
fi
popd
else
skipping
fi
}
# enter testfiles directory
if [[ -d $testfilespath ]]; then
cd "$testfilespath"
else
fail "specified testfiles directory does not exist"
exit -1
fi
download \
'Matroska Test Suite - Wave 1' \
'http://downloads.sourceforge.net/project/matroska/test_files/matroska_test_w1_1.zip?r=&ts=1454982254&use_mirror=netix' \
'matroska_test_w1_1.zip' \
'matroska_wave1'

View File

@ -123,7 +123,7 @@ inline std::string Margin::toString() const
{
std::stringstream res;
res << "top: " << m_top << "; left: " << m_left;
res << "bottom: " << m_bottom << "; right: " << m_right;
res << "; bottom: " << m_bottom << "; right: " << m_right;
return std::string(res.str());
}

View File

@ -7,6 +7,8 @@
#include "../wav/waveaudiostream.h"
#include "../avc/avcconfiguration.h"
#include "../mp4/mp4ids.h"
#include "../mp4/mp4track.h"
@ -411,6 +413,19 @@ void MatroskaTrack::internalParseHeader()
m_extensionChannelConfig = audioSpecificConfig->extensionChannelConfiguration;
}
break;
case GeneralMediaFormat::Avc:
if((codecPrivateElement = m_trackElement->childById(MatroskaIds::CodecPrivate))) {
auto avcConfig = make_unique<Media::AvcConfiguration>();
try {
m_istream->seekg(codecPrivateElement->dataOffset());
avcConfig->parse(m_reader, codecPrivateElement->dataSize());
Mp4Track::addInfo(*avcConfig, *this);
} catch(const TruncatedDataException &) {
addNotification(NotificationType::Critical, "AVC configuration is truncated.", context);
} catch(const Failure &) {
addNotification(NotificationType::Critical, "AVC configuration is invalid.", context);
}
}
default:
;
}

View File

@ -181,7 +181,7 @@ startParsingSignature:
goto startParsingSignature;
}
if(m_paddingSize) {
addNotification(NotificationType::Warning, ConversionUtilities::numberToString(m_paddingSize) + " zero-bytes skipped at the beginning of the file.", context);
addNotification(NotificationType::Warning, numberToString(m_paddingSize) + " zero-bytes skipped at the beginning of the file.", context);
}
// parse signature
@ -198,7 +198,7 @@ startParsingSignature:
stream().read(buff, 5);
// set the container offset to skip ID3v2 header
m_containerOffset += ConversionUtilities::toNormalInt(ConversionUtilities::BE::toUInt32(buff + 1)) + 10;
m_containerOffset += toNormalInt(BE::toUInt32(buff + 1)) + 10;
if((*buff) & 0x10) {
// footer present
m_containerOffset += 10;
@ -208,17 +208,19 @@ startParsingSignature:
goto startParsingSignature;
case ContainerFormat::Mp4: {
// EBML/Matroska is handled using Mp4Container instance
m_container = make_unique<Mp4Container>(*this, m_containerOffset);
NotificationList notifications;
try {
static_cast<Mp4Container *>(m_container.get())->validateElementStructure(notifications, &m_paddingSize);
} catch (Failure &) {
} catch(const Failure &) {
m_containerParsingStatus = ParsingStatus::CriticalFailure;
}
addNotifications(notifications);
break;
} case ContainerFormat::Ebml: {
// EBML/Matroska is handled using MatroskaContainer instance
auto container = make_unique<MatroskaContainer>(*this, m_containerOffset);
NotificationList notifications;
try {
@ -234,16 +236,28 @@ startParsingSignature:
container->validateElementStructure(notifications, &m_paddingSize);
container->validateIndex();
}
} catch(Failure &) {
} catch(const Failure &) {
m_containerParsingStatus = ParsingStatus::CriticalFailure;
}
m_container = move(container);
addNotifications(notifications);
break;
} case ContainerFormat::Ogg:
// Ogg is handled using OggContainer instance
m_container = make_unique<OggContainer>(*this, m_containerOffset);
static_cast<OggContainer *>(m_container.get())->setChecksumValidationEnabled(m_forceFullParse);
break;
case ContainerFormat::Unknown:
// container format is still unknown -> check for magic numbers at odd offsets
// -> check for tar (magic number at offset 0x101)
if(size() > 0x107) {
stream().seekg(0x101);
stream().read(buff, 6);
if(buff[0] == 0x75 && buff[1] == 0x73 && buff[2] == 0x74 && buff[3] == 0x61 && buff[4] == 0x72 && buff[5] == 0x00) {
m_containerFormat = ContainerFormat::Tar;
break;
}
}
default:
;
}
@ -306,10 +320,10 @@ void MediaFileInfo::parseTracks()
m_singleTrack->parseHeader();
}
m_tracksParsingStatus = ParsingStatus::Ok;
} catch (NotImplementedException &) {
} catch(const NotImplementedException &) {
addNotification(NotificationType::Information, "Parsing tracks is not implemented for the container format of the file.", context);
m_tracksParsingStatus = ParsingStatus::NotSupported;
} catch (Failure &) {
} catch(const Failure &) {
addNotification(NotificationType::Critical, "Unable to parse tracks.", context);
m_tracksParsingStatus = ParsingStatus::CriticalFailure;
}
@ -349,9 +363,9 @@ void MediaFileInfo::parseTags()
try {
m_id3v1Tag->parse(stream(), true);
m_actualExistingId3v1Tag = true;
} catch(NoDataFoundException &) {
} catch(const NoDataFoundException &) {
m_id3v1Tag.reset(); // no ID3v1 tag found
} catch(Failure &) {
} catch(const Failure &) {
m_tagsParsingStatus = ParsingStatus::CriticalFailure;
addNotification(NotificationType::Critical, "Unable to parse ID3v1 tag.", context);
}
@ -364,9 +378,9 @@ void MediaFileInfo::parseTags()
try {
id3v2Tag->parse(stream(), size() - offset);
m_paddingSize += id3v2Tag->paddingSize();
} catch(NoDataFoundException &) {
} catch(const NoDataFoundException &) {
continue;
} catch(Failure &) {
} catch(const Failure &) {
m_tagsParsingStatus = ParsingStatus::CriticalFailure;
addNotification(NotificationType::Critical, "Unable to parse ID3v2 tag.", context);
}
@ -375,13 +389,13 @@ void MediaFileInfo::parseTags()
if(m_container) {
try {
m_container->parseTags();
} catch (NotImplementedException &) {
} catch(const NotImplementedException &) {
if(m_tagsParsingStatus == ParsingStatus::NotParsedYet) {
// do not override parsing status from ID3 tags here
m_tagsParsingStatus = ParsingStatus::NotSupported;
}
addNotification(NotificationType::Information, "Parsing tags is not implemented for the container format of the file.", context);
} catch (Failure &) {
} catch(const Failure &) {
m_tagsParsingStatus = ParsingStatus::CriticalFailure;
addNotification(NotificationType::Critical, "Unable to parse tag.", context);
}

View File

@ -42,6 +42,7 @@ const char *MediaFormat::name() const
case GeneralMediaFormat::Amr: return "Adaptive Multi-Rate audio codec";
case GeneralMediaFormat::Avc:
switch(sub) {
case AvcCavlc444IntraProfile: return "Advanced Video Coding CAVLC 4:4:4 Intra Profile";
case AvcBaselineProfile: return "Advanced Video Coding Basline Profile";
case AvcMainProfile: return "Advanced Video Coding Main Profile";
case AvcScalableBaselineProfile: return "Advanced Video Coding Scalable Basline Profile";
@ -49,8 +50,12 @@ const char *MediaFormat::name() const
case AvcExtendedProfile: return "Advanced Video Coding Extended Profile";
case AvcHighProfile: return "Advanced Video Coding High Profile";
case AvcHigh10Profile: return "Advanced Video Coding High 10 Profile";
case AvcHighMultiviewProfile: return "Advanced Video Coding Multiview Profile";
case AvcHigh422Profile: return "Advanced Video Coding High 4:2:2 Profile";
case AvcStereoHighProfile: return "Advanced Video Coding Stereo High Profile";
case AvcHighMultiviewDepthProfile: return "Advanced Video Coding Multiview Depth High Profile";
case AvcHigh444Profile: return "Advanced Video Coding High 4:4:4 Profile";
case AvcHigh444PredictiveProfile: return "Advanced Video Coding High 4:4:4 Predictive Profile";
default: return "Advanced Video Coding";
}
case GeneralMediaFormat::Bitmap: return "Windows Bitmap";
@ -183,6 +188,7 @@ const char *MediaFormat::name() const
case Mpeg4FineGranularityScalableProfile5: return "MPEG-4 Fine Granularity Scalable Profile L5";
default: return "MPEG-4 Visual";
}
case GeneralMediaFormat::Mpeg4TimedText: return "MPEG-4 Timed Text";
case GeneralMediaFormat::Mpc: return "Musepack SV8";
case GeneralMediaFormat::Pcm:
switch(sub) {

View File

@ -12,11 +12,11 @@ namespace Media {
*/
enum class MediaType
{
Unknown,
Audio,
Video,
Text,
Hint
Unknown, /**< the type is unknown */
Audio, /**< audio/sound */
Video, /**< video */
Text, /**< text/subtitle */
Hint /**< hint */
};
/*!
@ -24,7 +24,7 @@ enum class MediaType
*/
enum class GeneralMediaFormat
{
Unknown,
Unknown, /**< the format is unknown */
Aac, /**< Advanced Video Coding */
Ac3, /**< Dolby Digital */
Ac4, /**< AC-4 */
@ -50,16 +50,17 @@ enum class GeneralMediaFormat
InteractionStream, /**< Interaction Stream */
Jpeg, /**< JPEG */
OggKate, /**< Karaoke And Text Encapsulation */
Opus = 64, /**< Opus */
MicrosoftAudioCodecManager = 26, /**< Microsoft Audio Codec Manager (ACM) */
Opus, /**< Opus */
MicrosoftAudioCodecManager, /**< Microsoft Audio Codec Manager (ACM) */
MicrosoftMpeg4, /**< Microsoft MPEG-4 */
MicrosoftVideoCodecManager, /**< Microsoft Video Codec Manager (VCM) */
DolbyMlp, /** < Dolby MLP */
Mpeg1Audio, /**< MPEG-1 Audio */
Mpeg1Video, /**< MPEG-1 Vudio */
Mpeg2Audio, /**< MPEG-2 Audio */
Mpeg2Video, /**< MPEG-2 Vudio */
Mpeg4Video, /**< MPEG-4 */
Mpeg2Video, /**< MPEG-2 Video */
Mpeg4Video, /**< MPEG-4 Video */
Mpeg4TimedText, /**< MPEG-4 Timed Text / Streaming text format / Part 17 */
Mpc, /**< Musepack */
Pcm, /**< Pulse Code Modulation */
Png, /**< PNG */
@ -195,6 +196,7 @@ enum Mpeg4VideoProfile : unsigned char {
};
enum AvcProfile : unsigned char {
AvcCavlc444IntraProfile = 0x2C,
AvcBaselineProfile = 0x42,
AvcMainProfile = 0x4D,
AvcScalableBaselineProfile = 0x53,
@ -202,8 +204,12 @@ enum AvcProfile : unsigned char {
AvcExtendedProfile = 0x58,
AvcHighProfile = 0x64,
AvcHigh10Profile = 0x6E,
AvcHighMultiviewProfile = 0x76,
AvcHigh422Profile = 0x7A,
AvcHigh444Profile = 0x90
AvcStereoHighProfile = 0x80,
AvcHighMultiviewDepthProfile = 0x8A,
AvcHigh444Profile = 0x90,
AvcHigh444PredictiveProfile = 0xF4
};
enum DtsSpecifier : unsigned char {

View File

@ -51,6 +51,8 @@ MediaFormat fourccToMediaFormat(uint32 fourccId)
return GeneralMediaFormat::Mpeg2Video;
case Mpeg4Video:
return GeneralMediaFormat::Mpeg4Video;
case Mpeg4TimedText:
return GeneralMediaFormat::Mpeg4TimedText;
case Hevc1: case Hevc2:
return MediaFormat(GeneralMediaFormat::Hevc);
case Avc1: case Avc2: case Avc3: case Avc4: case H264Decoder1: case H264Decoder2:

View File

@ -318,6 +318,7 @@ enum KnownValue : uint32 {
Mpeg4Decoder2 = 0x53454447,
Mpeg4Decoder3 = 0x57563146,
Mpeg4Sample = 0x6d703473, /**< MPEG-4 stream (other then video/audio) */
Mpeg4TimedText = 0x74783367, /**< MPEG-4 Timed Text / Streaming text format / Part 17 */
Mpeg4Video = 0x6d703476, /**< MPEG-4 video */
MsMpeg4V1Decoder1 = 0x44495631,
MsMpeg4V1Decoder2 = 0x64697631,

View File

@ -7,6 +7,7 @@
#include <c++utilities/io/binarywriter.h>
#include <vector>
#include <sstream>
namespace Media
{

View File

@ -4,6 +4,8 @@
#include "./mp4ids.h"
#include "./mpeg4descriptor.h"
#include "../avc/avcconfiguration.h"
#include "../mpegaudio/mpegaudioframe.h"
#include "../mpegaudio/mpegaudioframestream.h"
@ -48,7 +50,8 @@ Mpeg4AudioSpecificConfig::Mpeg4AudioSpecificConfig() :
epConfig(0)
{}
Mpeg4VideoSpecificConfig::Mpeg4VideoSpecificConfig()
Mpeg4VideoSpecificConfig::Mpeg4VideoSpecificConfig() :
profile(0)
{}
/*!
@ -423,63 +426,6 @@ vector<uint64> Mp4Track::readChunkSizes()
return chunkSizes;
}
#ifdef UNDER_CONSTRUCTION
/*!
* \brief Reads the AVC configuration for the track.
* \remarks
* - Returns an empty configuration for non-AVC tracks.
* - Notifications might be added.
*/
AvcConfiguration Mp4Track::parseAvcConfiguration(StatusProvider &statusProvider, BinaryReader &reader, uint64 startOffset, uint64 size)
{
AvcConfiguration config;
try {
if(size >= 5) {
// skip first byte (is always 1)
reader.stream()->seekg(startOffset + 1);
// read profile, IDC level, NALU size length
config.profileIdc = reader.readByte();
config.profileCompat = reader.readByte();
config.levelIdc = reader.readByte();
config.naluSizeLength = reader.readByte() & 0x03;
// read SPS infos
if((size -= 5) >= 3) {
byte entryCount = reader.readByte() & 0x0f;
uint16 entrySize;
while(entryCount && size) {
if((entrySize = reader.readUInt16BE()) <= size) {
// TODO: read entry
size -= entrySize;
} else {
throw TruncatedDataException();
}
--entryCount;
}
// read PPS infos
if((size -= 5) >= 3) {
entryCount = reader.readByte();
while(entryCount && size) {
if((entrySize = reader.readUInt16BE()) <= size) {
// TODO: read entry
size -= entrySize;
} else {
throw TruncatedDataException();
}
--entryCount;
}
// TODO: read trailer
return config;
}
}
}
throw TruncatedDataException();
} catch (TruncatedDataException &) {
statusProvider.addNotification(NotificationType::Critical, "AVC configuration is truncated.", "parsing AVC configuration");
}
return config;
}
#endif
/*!
* \brief Reads the MPEG-4 elementary stream descriptor for the track.
* \remarks
@ -761,8 +707,7 @@ std::unique_ptr<Mpeg4VideoSpecificConfig> Mp4Track::parseVideoSpecificConfig(Sta
default:
;
}
// skip stuff we're not interested in to get the start of the
// next video object
// skip remainging values to get the start of the next video object
while(size >= 3) {
if(reader.readUInt24BE() != 1) {
reader.stream()->seekg(-2, ios_base::cur);
@ -915,6 +860,41 @@ void Mp4Track::updateChunkOffset(uint32 chunkIndex, uint64 offset)
}
}
/*!
* \brief Adds the information from the specified \a avcConfig to the specified \a track.
*/
void Mp4Track::addInfo(const AvcConfiguration &avcConfig, AbstractTrack &track)
{
if(!avcConfig.spsInfos.empty()) {
const SpsInfo &spsInfo = avcConfig.spsInfos.back();
track.m_format.sub = spsInfo.profileIndication;
track.m_version = static_cast<double>(spsInfo.levelIndication) / 10;
track.m_cropping = spsInfo.cropping;
track.m_pixelSize = spsInfo.pictureSize;
switch(spsInfo.chromaFormatIndication) {
case 0:
track.m_chromaFormat = "monochrome";
break;
case 1:
track.m_chromaFormat = "YUV 4:2:0";
break;
case 2:
track.m_chromaFormat = "YUV 4:2:2";
break;
case 3:
track.m_chromaFormat = "YUV 4:4:4";
break;
default:
;
}
track.m_pixelAspectRatio = spsInfo.pixelAspectRatio;
} else {
track.m_format.sub = avcConfig.profileIndication;
track.m_version = static_cast<double>(avcConfig.levelIndication) / 10;
}
}
/*!
* \brief Makes the track entry ("trak"-atom) for the track. The data is written to the assigned output stream
* at the current position.
@ -1285,23 +1265,28 @@ void Mp4Track::internalParseHeader()
buff[2] = ((rawLanguage & 0x001F) >> 0x0) + 0x60;
m_language = string(buff, 3);
// read hdlr atom
// track type
m_istream->seekg(m_hdlrAtom->startOffset() + 16); // seek to beg, skip size, name, version, flags and reserved bytes
string trackTypeStr = reader.readString(4);
if(trackTypeStr == "soun") {
m_mediaType = MediaType::Audio;
} else if(trackTypeStr == "vide") {
// -> seek to begin skipping size, name, version, flags and reserved bytes
m_istream->seekg(m_hdlrAtom->dataOffset() + 8);
// -> track type
switch(reader.readUInt32BE()) {
case 0x76696465:
m_mediaType = MediaType::Video;
} else if(trackTypeStr == "hint") {
break;
case 0x736F756E:
m_mediaType = MediaType::Audio;
break;
case 0x68696E74:
m_mediaType = MediaType::Hint;
} else if(trackTypeStr == "meta") {
break;
case 0x6D657461: case 0x74657874:
m_mediaType = MediaType::Text;
} else {
break;
default:
m_mediaType = MediaType::Unknown;
}
// name
m_istream->seekg(12, ios_base::cur); // skip reserved bytes
//name = reader.readString(hdlrAtom->size - 16 - 4 - 12);
m_name = reader.readTerminatedString(m_hdlrAtom->totalSize() - 12 - 4 - 12, 0);
// read stco atom (only chunk count)
m_chunkOffsetSize = (m_stcoAtom->id() == Mp4AtomIds::ChunkOffset64) ? 8 : 4;
@ -1380,7 +1365,7 @@ void Mp4Track::internalParseHeader()
}
break;
case FourccIds::Mpeg4Sample:
//m_istream->seekg(6 + 2, ios_base::cur); // skip reserved bytes and data reference index
// skip reserved bytes and data reference index
codecConfigContainerAtom->denoteFirstChild(codecConfigContainerAtom->headerSize() + 8);
if(!esDescParentAtom) {
esDescParentAtom = codecConfigContainerAtom;
@ -1394,10 +1379,23 @@ void Mp4Track::internalParseHeader()
;
}
}
// parse AVC configuration
//codecConfigContainerAtom->childById(Mp4AtomIds::AvcConfiguration);
// parse MPEG-4 elementary stream descriptor
if(esDescParentAtom) {
// parse AVC configuration
if(Mp4Atom *avcConfigAtom = esDescParentAtom->childById(Mp4AtomIds::AvcConfiguration)) {
m_istream->seekg(avcConfigAtom->dataOffset());
m_avcConfig = make_unique<Media::AvcConfiguration>();
try {
m_avcConfig->parse(reader, avcConfigAtom->dataSize());
addInfo(*m_avcConfig, *this);
} catch(const TruncatedDataException &) {
addNotification(NotificationType::Critical, "AVC configuration is truncated.", context);
} catch(const Failure &) {
addNotification(NotificationType::Critical, "AVC configuration is invalid.", context);
}
}
// parse MPEG-4 elementary stream descriptor
Mp4Atom *esDescAtom = esDescParentAtom->childById(Mp4FormatExtensionIds::Mpeg4ElementaryStreamDescriptor);
if(!esDescAtom) {
esDescAtom = esDescParentAtom->childById(Mp4FormatExtensionIds::Mpeg4ElementaryStreamDescriptor2);

View File

@ -1,10 +1,6 @@
#ifndef MP4TRACK_H
#define MP4TRACK_H
#ifdef UNDER_CONSTRUCTION
#include "../avc/avcconfiguration.h"
#endif
#include "../abstracttrack.h"
#include <vector>
@ -15,6 +11,7 @@ namespace Media
class Mp4Atom;
class Mpeg4Descriptor;
class AvcConfiguration;
class LIB_EXPORT Mpeg4AudioSpecificConfig
{
@ -133,11 +130,9 @@ public:
uint32 chunkCount() const;
uint32 sampleToChunkEntryCount() const;
const Mpeg4ElementaryStreamInfo *mpeg4ElementaryStreamInfo() const;
const AvcConfiguration *avcConfiguration() const;
// methods to parse configuration details from the track header
#ifdef UNDER_CONSTRUCTION
static AvcConfiguration parseAvcConfiguration(StatusProvider &statusProvider, IoUtilities::BinaryReader &reader, uint64 startOffset, uint64 size);
#endif
static std::unique_ptr<Mpeg4ElementaryStreamInfo> parseMpeg4ElementaryStreamInfo(StatusProvider &statusProvider, IoUtilities::BinaryReader &reader, Mp4Atom *esDescAtom);
static std::unique_ptr<Mpeg4AudioSpecificConfig> parseAudioSpecificConfig(StatusProvider &statusProvider, std::istream &stream, uint64 startOffset, uint64 size);
static std::unique_ptr<Mpeg4VideoSpecificConfig> parseVideoSpecificConfig(StatusProvider &statusProvider, IoUtilities::BinaryReader &reader, uint64 startOffset, uint64 size);
@ -160,6 +155,8 @@ public:
void updateChunkOffsets(const std::vector<uint64> &chunkOffsets);
void updateChunkOffset(uint32 chunkIndex, uint64 offset);
static void addInfo(const AvcConfiguration &avcConfig, AbstractTrack &track);
protected:
void internalParseHeader();
@ -187,6 +184,7 @@ private:
uint32 m_chunkCount;
uint32 m_sampleToChunkEntryCount;
std::unique_ptr<Mpeg4ElementaryStreamInfo> m_esInfo;
std::unique_ptr<AvcConfiguration> m_avcConfig;
};
/*!
@ -238,20 +236,26 @@ inline uint32 Mp4Track::sampleToChunkEntryCount() const
/*!
* \brief Returns information about the MPEG-4 elementary stream.
* \remarks
* - The Mp4Track::readMpeg4ElementaryStreamInfo() method must be called before
* to parse the information. This is done when parsing the track.
* - The information is only available, if the track has an MPEG-4 elementary stream
* descriptor atom.
* - The track must be parsed before this information becomes available.
* - The information is only available, if the track has an MPEG-4 elementary stream descriptor atom.
* - The track keeps ownership over the returned object.
* \sa
* - readMpeg4ElementaryStreamInfo()
* - hasMpeg4ElementaryStreamDesc()
*/
inline const Mpeg4ElementaryStreamInfo *Mp4Track::mpeg4ElementaryStreamInfo() const
{
return m_esInfo.get();
}
/*!
* \brief Returns the AVC configuration.
* \remarks
* - The track must be parsed before this information becomes available.
* - The track keeps ownership over the returned object.
*/
inline const AvcConfiguration *Mp4Track::avcConfiguration() const
{
return m_avcConfig.get();
}
}
#endif // MP4TRACK_H

View File

@ -58,8 +58,7 @@ void MpegAudioFrameStream::internalParseHeader()
m_bitrate = frame.isXingFramefieldPresent()
? ((static_cast<double>(m_size) * 8.0) / (static_cast<double>(frame.xingFrameCount() * frame.sampleCount()) / static_cast<double>(frame.samplingFrequency())) / 1024.0)
: frame.bitrate();
m_bytesPerSecond = m_bitrate * 125;
m_duration = TimeSpan::fromSeconds(static_cast<double>(m_size) / (m_bitrate * 128.0));
m_duration = TimeSpan::fromSeconds(static_cast<double>(m_size) / (m_bytesPerSecond = m_bitrate * 125));
}
}

View File

@ -25,9 +25,6 @@ OggContainer::OggContainer(MediaFileInfo &fileInfo, uint64 startOffset) :
m_validateChecksums(false)
{}
/*!
* \brief Destroys the container.
*/
OggContainer::~OggContainer()
{}
@ -42,7 +39,6 @@ void OggContainer::internalParseHeader()
static const string context("parsing OGG bitstream header");
// iterate through pages using OggIterator helper class
try {
uint32 pageSequenceNumber = 0;
// ensure iterator is setup properly
for(m_iterator.removeFilter(), m_iterator.reset(); m_iterator; m_iterator.nextPage()) {
const OggPage &page = m_iterator.currentPage();
@ -51,25 +47,29 @@ void OggContainer::internalParseHeader()
addNotification(NotificationType::Warning, "The denoted checksum of the OGG page at " + ConversionUtilities::numberToString(m_iterator.currentSegmentOffset()) + " does not match the computed checksum.", context);
}
}
if(!m_streamsBySerialNo.count(page.streamSerialNumber())) {
OggStream *stream;
try {
stream = m_tracks[m_streamsBySerialNo.at(page.streamSerialNumber())].get();
} catch(const out_of_range &) {
// new stream serial number recognized -> add new stream
m_streamsBySerialNo[page.streamSerialNumber()] = m_tracks.size();
m_tracks.emplace_back(new OggStream(*this, m_iterator.currentPageIndex()));
m_tracks.emplace_back(make_unique<OggStream>(*this, m_iterator.currentPageIndex()));
stream = m_tracks.back().get();
}
if(pageSequenceNumber != page.sequenceNumber()) {
if(pageSequenceNumber != 0) {
if(stream->m_currentSequenceNumber != page.sequenceNumber()) {
if(stream->m_currentSequenceNumber) {
addNotification(NotificationType::Warning, "Page is missing (page sequence number omitted).", context);
pageSequenceNumber = page.sequenceNumber();
}
stream->m_currentSequenceNumber = page.sequenceNumber() + 1;
} else {
++pageSequenceNumber;
++stream->m_currentSequenceNumber;
}
}
} catch(TruncatedDataException &) {
} catch(const TruncatedDataException &) {
// thrown when page exceeds max size
addNotification(NotificationType::Critical, "The OGG file is truncated.", context);
throw;
} catch(InvalidDataException &) {
} catch(const InvalidDataException &) {
// thrown when first 4 byte do not match capture pattern
addNotification(NotificationType::Critical, "Capture pattern \"OggS\" at " + ConversionUtilities::numberToString(m_iterator.currentSegmentOffset()) + " expected.", context);
throw;
@ -79,19 +79,28 @@ void OggContainer::internalParseHeader()
void OggContainer::internalParseTags()
{
parseTracks(); // tracks needs to be parsed because tags are stored at stream level
for(auto &i : m_commentTable) {
//fileInfo().stream().seekg(get<1>(i));
m_iterator.setPageIndex(i.firstPageIndex);
m_iterator.setSegmentIndex(i.firstSegmentIndex);
m_tags[i.tagIndex]->parse(m_iterator);
i.lastPageIndex = m_iterator.currentPageIndex();
i.lastSegmentIndex = m_iterator.currentSegmentIndex();
for(VorbisCommentInfo &vorbisCommentInfo : m_commentTable) {
m_iterator.setPageIndex(vorbisCommentInfo.firstPageIndex);
m_iterator.setSegmentIndex(vorbisCommentInfo.firstSegmentIndex);
switch(vorbisCommentInfo.streamFormat) {
case GeneralMediaFormat::Vorbis:
m_tags[vorbisCommentInfo.tagIndex]->parse(m_iterator);
break;
case GeneralMediaFormat::Opus:
m_iterator.seekForward(8); // skip header (has already been detected by OggStream)
m_tags[vorbisCommentInfo.tagIndex]->parse(m_iterator, true);
break;
default:
addNotification(NotificationType::Critical, "Stream format not supported.", "parsing tags from OGG streams");
}
vorbisCommentInfo.lastPageIndex = m_iterator.currentPageIndex();
vorbisCommentInfo.lastSegmentIndex = m_iterator.currentSegmentIndex();
}
}
void OggContainer::ariseComment(vector<OggPage>::size_type pageIndex, vector<uint32>::size_type segmentIndex)
void OggContainer::ariseComment(vector<OggPage>::size_type pageIndex, vector<uint32>::size_type segmentIndex, GeneralMediaFormat mediaFormat)
{
m_commentTable.emplace_back(pageIndex, segmentIndex, m_tags.size());
m_commentTable.emplace_back(pageIndex, segmentIndex, m_tags.size(), mediaFormat);
m_tags.emplace_back(make_unique<VorbisComment>());
}
@ -99,14 +108,14 @@ void OggContainer::internalParseTracks()
{
if(!areTracksParsed()) {
parseHeader();
static const string context("parsing OGG bit streams");
static const string context("parsing OGG stream");
for(auto &stream : m_tracks) {
try { // try to parse header
stream->parseHeader();
if(stream->duration() > m_duration) {
m_duration = stream->duration();
}
} catch(Failure &) {
} catch(const Failure &) {
addNotification(NotificationType::Critical, "Unable to parse stream at " + ConversionUtilities::numberToString(stream->startOffset()) + ".", context);
}
}
@ -154,7 +163,18 @@ void OggContainer::internalMakeFile()
if(m_iterator.currentPageIndex() == commentTableIterator->firstPageIndex) {
// make Vorbis Comment segment
auto offset = buffer.tellp();
m_tags[commentTableIterator->tagIndex]->make(buffer);
switch(commentTableIterator->streamFormat) {
case GeneralMediaFormat::Vorbis:
m_tags[commentTableIterator->tagIndex]->make(buffer);
break;
case GeneralMediaFormat::Opus:
ConversionUtilities::BE::getBytes(0x4F70757354616773u, copy.buffer());
buffer.write(copy.buffer(), 8);
m_tags[commentTableIterator->tagIndex]->make(buffer, true);
break;
default:
;
}
newSegmentSizes.push_back(buffer.tellp() - offset);
}
if(m_iterator.currentPageIndex() > commentTableIterator->lastPageIndex

View File

@ -18,21 +18,23 @@ class MediaFileInfo;
struct LIB_EXPORT VorbisCommentInfo
{
VorbisCommentInfo(std::vector<OggPage>::size_type firstPageIndex, std::vector<OggPage>::size_type firstSegmentIndex, std::vector<OggPage>::size_type tagIndex);
VorbisCommentInfo(std::vector<OggPage>::size_type firstPageIndex, std::vector<OggPage>::size_type firstSegmentIndex, std::vector<OggPage>::size_type tagIndex, GeneralMediaFormat streamFormat = GeneralMediaFormat::Vorbis);
std::vector<OggPage>::size_type firstPageIndex;
std::vector<OggPage>::size_type firstSegmentIndex;
std::vector<OggPage>::size_type lastPageIndex;
std::vector<OggPage>::size_type lastSegmentIndex;
std::vector<std::unique_ptr<VorbisComment> >::size_type tagIndex;
GeneralMediaFormat streamFormat;
};
inline VorbisCommentInfo::VorbisCommentInfo(std::vector<OggPage>::size_type firstPageIndex, std::vector<OggPage>::size_type firstSegmentIndex, std::vector<OggPage>::size_type tagIndex) :
inline VorbisCommentInfo::VorbisCommentInfo(std::vector<OggPage>::size_type firstPageIndex, std::vector<OggPage>::size_type firstSegmentIndex, std::vector<OggPage>::size_type tagIndex, GeneralMediaFormat mediaFormat) :
firstPageIndex(firstPageIndex),
firstSegmentIndex(firstSegmentIndex),
lastPageIndex(0),
lastSegmentIndex(0),
tagIndex(tagIndex)
tagIndex(tagIndex),
streamFormat(mediaFormat)
{}
class LIB_EXPORT OggContainer : public GenericContainer<MediaFileInfo, VorbisComment, OggStream, OggPage>
@ -54,7 +56,7 @@ protected:
void internalMakeFile();
private:
void ariseComment(std::vector<OggPage>::size_type pageIndex, std::vector<uint32>::size_type segmentIndex);
void ariseComment(std::vector<OggPage>::size_type pageIndex, std::vector<uint32>::size_type segmentIndex, GeneralMediaFormat mediaFormat = GeneralMediaFormat::Vorbis);
std::unordered_map<uint32, std::vector<std::unique_ptr<OggStream> >::size_type> m_streamsBySerialNo;

View File

@ -15,6 +15,8 @@ public:
std::istream &stream();
void setStream(std::istream &stream);
uint64 startOffset() const;
uint64 streamSize() const;
void reset();
void nextPage();
void nextSegment();
@ -34,6 +36,7 @@ public:
bool areAllPagesFetched() const;
void read(char *buffer, size_t count);
void seekForward(size_t count);
bool bytesRemaining(size_t atLeast) const;
operator bool() const;
OggIterator &operator ++();
@ -92,6 +95,22 @@ inline void OggIterator::setStream(std::istream &stream)
m_stream = &stream;
}
/*!
* \brief Returns the start offset (which has been specified when constructing the iterator).
*/
inline uint64 OggIterator::startOffset() const
{
return m_startOffset;
}
/*!
* \brief Returns the stream size (which has been specified when constructing the iterator).
*/
inline uint64 OggIterator::streamSize() const
{
return m_streamSize;
}
/*!
* \brief Returns a vector of containing the OGG pages that have been fetched yet.
*/
@ -226,6 +245,14 @@ inline bool OggIterator::areAllPagesFetched() const
return (m_pages.empty() ? m_startOffset : m_pages.back().startOffset() + m_pages.back().totalSize()) >= m_streamSize;
}
/*!
* \brief Returns whether there are \a atLeast bytes remaining.
*/
inline bool OggIterator::bytesRemaining(size_t atLeast) const
{
return *this && currentCharacterOffset() + atLeast <= streamSize();
}
/*!
* \brief Increments the current position by one segment if the iterator is valid; otherwise nothing happens.
*/

View File

@ -4,6 +4,8 @@
#include "../vorbis/vorbispackagetypes.h"
#include "../vorbis/vorbisidentificationheader.h"
#include "../opus/opusidentificationheader.h"
#include "../mediafileinfo.h"
#include "../exceptions.h"
#include "../mediaformat.h"
@ -28,7 +30,8 @@ namespace Media {
OggStream::OggStream(OggContainer &container, vector<OggPage>::size_type startPage) :
AbstractTrack(container.stream(), container.m_iterator.pages()[startPage].startOffset()),
m_startPage(startPage),
m_container(container)
m_container(container),
m_currentSequenceNumber(0)
{}
/*!
@ -52,12 +55,12 @@ void OggStream::internalParseHeader()
bool hasIdentificationHeader = false;
bool hasCommentHeader = false;
for(; iterator; ++iterator) {
uint32 currentSize = iterator.currentSegmentSize();
const uint32 currentSize = iterator.currentSegmentSize();
m_size += currentSize;
if(currentSize >= 8) {
// determine stream format
inputStream().seekg(iterator.currentSegmentOffset());
uint64 sig = reader().readUInt64BE();
const uint64 sig = reader().readUInt64BE();
if((sig & 0x00ffffffffffff00u) == 0x00766F7262697300u) {
// Vorbis header detected
// set Vorbis as format
@ -75,8 +78,10 @@ void OggStream::internalParseHeader()
switch(sig >> 56) {
case VorbisPackageTypes::Identification:
if(!hasIdentificationHeader) {
// parse identification header
VorbisIdentificationHeader ind;
ind.parseHeader(iterator);
m_version = ind.version();
m_channelCount = ind.channels();
m_samplingFrequency = ind.sampleRate();
if(ind.nominalBitrate()) {
@ -87,6 +92,7 @@ void OggStream::internalParseHeader()
if(m_bitrate) {
m_bitrate = static_cast<double>(m_bitrate) / 1000.0;
}
// determine sample count and duration if all pages have been fetched
if(iterator.areAllPagesFetched()) {
auto pred = [this] (const OggPage &page) -> bool {
return page.streamSerialNumber() == this->id();
@ -105,9 +111,9 @@ void OggStream::internalParseHeader()
}
break;
case VorbisPackageTypes::Comments:
// Vorbis comment found -> notify container about comment
if(!hasCommentHeader) {
//m_container.m_commentOffsets.push_back(iterator.currentOffset());
m_container.ariseComment(iterator.currentPageIndex(), iterator.currentSegmentIndex());
m_container.ariseComment(iterator.currentPageIndex(), iterator.currentSegmentIndex(), GeneralMediaFormat::Vorbis);
hasCommentHeader = true;
} else {
addNotification(NotificationType::Critical, "Vorbis comment header appears more then once. Oversupplied occurrence will be ignored.", context);
@ -118,7 +124,84 @@ void OggStream::internalParseHeader()
default:
;
}
} // currently only Vorbis supported
} else if(sig == 0x4F70757348656164u) {
// Opus header detected
// set Opus as format
switch(m_format.general) {
case GeneralMediaFormat::Unknown:
m_format = GeneralMediaFormat::Opus;
m_mediaType = MediaType::Audio;
break;
case GeneralMediaFormat::Opus:
break;
default:
addNotification(NotificationType::Warning, "Stream format is inconsistent.", context);
}
if(!hasIdentificationHeader) {
// parse identification header
OpusIdentificationHeader ind;
ind.parseHeader(iterator);
m_version = ind.version();
m_channelCount = ind.channels();
m_samplingFrequency = ind.sampleRate();
// determine sample count and duration if all pages have been fetched
if(iterator.areAllPagesFetched()) {
auto pred = [this] (const OggPage &page) -> bool {
return page.streamSerialNumber() == this->id();
};
const auto &pages = iterator.pages();
auto firstPage = find_if(pages.cbegin(), pages.cend(), pred);
auto lastPage = find_if(pages.crbegin(), pages.crend(), pred);
if(firstPage != pages.cend() && lastPage != pages.crend()) {
m_sampleCount = lastPage->absoluteGranulePosition() - firstPage->absoluteGranulePosition();
// must apply "pre-skip" here do calculate effective sample count and duration?
if(m_sampleCount > ind.preSkip()) {
m_sampleCount -= ind.preSkip();
} else {
m_sampleCount = 0;
}
m_duration = TimeSpan::fromSeconds(static_cast<double>(m_sampleCount) / m_samplingFrequency);
}
}
hasIdentificationHeader = true;
} else {
addNotification(NotificationType::Critical, "Opus identification header appears more then once. Oversupplied occurrence will be ignored.", context);
}
} else if(sig == 0x4F70757354616773u) {
// Opus comment detected
// set Opus as format
switch(m_format.general) {
case GeneralMediaFormat::Unknown:
m_format = GeneralMediaFormat::Opus;
m_mediaType = MediaType::Audio;
break;
case GeneralMediaFormat::Opus:
break;
default:
addNotification(NotificationType::Warning, "Stream format is inconsistent.", context);
}
// notify container about comment
if(!hasCommentHeader) {
m_container.ariseComment(iterator.currentPageIndex(), iterator.currentSegmentIndex(), GeneralMediaFormat::Opus);
hasCommentHeader = true;
} else {
addNotification(NotificationType::Critical, "Opus tags/comment header appears more then once. Oversupplied occurrence will be ignored.", context);
}
} else if((sig & 0x00ffffffffffff00u) == 0x007468656F726100u) {
// Theora header detected
// set Theora as format
switch(m_format.general) {
case GeneralMediaFormat::Unknown:
m_format = GeneralMediaFormat::Theora;
m_mediaType = MediaType::Video;
break;
case GeneralMediaFormat::Theora:
break;
default:
addNotification(NotificationType::Warning, "Stream format is inconsistent.", context);
}
// TODO: read more information about Theora stream
} // currently only Vorbis, Opus and Theora can be detected
}
}
if(m_duration.isNull() && m_size && m_bitrate) {

View File

@ -26,6 +26,7 @@ protected:
private:
std::vector<OggPage>::size_type m_startPage;
OggContainer &m_container;
uint32 m_currentSequenceNumber;
};
inline TrackType OggStream::type() const

View File

@ -0,0 +1,40 @@
#include "./opusidentificationheader.h"
#include "../ogg/oggiterator.h"
#include "../exceptions.h"
#include <c++utilities/conversion/binaryconversion.h>
using namespace std;
using namespace ConversionUtilities;
namespace Media {
/*!
* \class Media::OpusIdentificationHeader
* \brief The OpusIdentificationHeader class is an Opus identification header parser.
* \sa https://wiki.xiph.org/OggOpus
*/
/*!
* \brief Parses the Opus identification header which is read using the specified \a iterator.
* \remarks The header is assumed to start at the current position of \a iterator.
*/
void OpusIdentificationHeader::parseHeader(OggIterator &iterator)
{
char buff[19 - 8];
iterator.read(buff, 8);
if(BE::toUInt64(buff) != 0x4F70757348656164u) {
throw InvalidDataException(); // not Opus identification header
}
iterator.read(buff, sizeof(buff));
m_version = static_cast<byte>(*(buff));
m_channels = static_cast<byte>(*(buff + 1));
m_preSkip = LE::toUInt16(buff + 2);
m_sampleRate = LE::toUInt32(buff + 4);
m_outputGain = LE::toUInt16(buff + 8);
m_channelMap = static_cast<byte>(*(buff + 10));
}
}

View File

@ -0,0 +1,110 @@
#ifndef MEDIA_OPUSIDENTIFICATIONHEADER_H
#define MEDIA_OPUSIDENTIFICATIONHEADER_H
#include <c++utilities/application/global.h>
#include <c++utilities/conversion/types.h>
#include <istream>
namespace Media {
class OggIterator;
class LIB_EXPORT OpusIdentificationHeader
{
public:
OpusIdentificationHeader();
void parseHeader(OggIterator &iterator);
byte version() const;
byte channels() const;
uint16 preSkip() const;
uint32 sampleRate() const;
uint16 outputGain() const;
byte channelMap() const;
private:
byte m_version;
byte m_channels;
uint16 m_preSkip;
uint32 m_sampleRate;
uint16 m_outputGain;
byte m_channelMap;
};
/*!
* \brief Constructs a new vorbis identification header.
*/
inline OpusIdentificationHeader::OpusIdentificationHeader() :
m_version(0),
m_channels(0),
m_sampleRate(0),
m_outputGain(0),
m_channelMap(0)
{}
/*!
* \brief Returns the version (which should be 1 currently).
*/
inline byte OpusIdentificationHeader::version() const
{
return m_version;
}
/*!
* \brief Returns the number of channels for the Opus stream.
*/
inline byte OpusIdentificationHeader::channels() const
{
return m_channels;
}
/*!
* \brief Returns "pre-skip" value for the Opus stream.
*
* This is the number of samples (at 48 kHz) to discard from the decoder
* output when starting playback, and also the number to subtract from a
* page's granule position to calculate its PCM sample position.
*/
inline uint16 OpusIdentificationHeader::preSkip() const
{
return m_preSkip;
}
/*!
* \brief Returns the INPUT sample rate.
* \remarks This is not the sample rate to use for playback of the encoded data.
* \sa https://wiki.xiph.org/OggOpus
*/
inline uint32 OpusIdentificationHeader::sampleRate() const
{
return m_sampleRate;
}
/*!
* \brief Returns the output gain.
*
* This is a gain to be applied by the decoder. Virtually all players and media frameworks
* should apply it by default.
*/
inline uint16 OpusIdentificationHeader::outputGain() const
{
return m_outputGain;
}
/*!
* \brief Returns the channel mapping family.
*
* The channel mapping family indicates the order and semantic meaning of the various channels
* encoded in each Opus packet.
* \sa https://wiki.xiph.org/OggOpus
*/
inline byte OpusIdentificationHeader::channelMap() const
{
return m_channelMap;
}
}
#endif // MEDIA_OPUSIDENTIFICATIONHEADER_H

View File

@ -11,6 +11,7 @@ namespace Media {
*/
enum Sig64 : uint64
{
Ar = 0x213C617263683E0A,
Asf1 = 0x3026B2758E66CF11ul,
Asf2 = 0xA6D900AA0062CE6Cul,
Png = 0x89504E470D0A1A0Aul,
@ -109,6 +110,8 @@ ContainerFormat parseSignature(const char *buffer, int bufferSize)
}
// return corresponding container format
switch(sig) { // check 64-bit signatures
case Ar:
return ContainerFormat::Ar;
case Asf1:
return ContainerFormat::Asf;
case Asf2:
@ -223,6 +226,7 @@ ContainerFormat parseSignature(const char *buffer, int bufferSize)
const char *containerFormatAbbreviation(ContainerFormat containerFormat, MediaType mediaType, unsigned int version)
{
switch(containerFormat) {
case ContainerFormat::Ar: return "a";
case ContainerFormat::Asf: return "asf";
case ContainerFormat::Elf: return "elf";
case ContainerFormat::Gif87a:
@ -273,6 +277,7 @@ const char *containerFormatAbbreviation(ContainerFormat containerFormat, MediaTy
case ContainerFormat::Riff: return "riff";
case ContainerFormat::RiffWave: return "wav";
case ContainerFormat::RiffAvi: return "avi";
case ContainerFormat::Tar: return "tar";
case ContainerFormat::TiffBigEndian:
case ContainerFormat::TiffLittleEndian: return "tiff";
case ContainerFormat::WindowsBitmap: return "bmp";
@ -296,6 +301,8 @@ const char *containerFormatName(ContainerFormat containerFormat)
switch(containerFormat) {
case ContainerFormat::Adts:
return "Audio Data Transport Stream";
case ContainerFormat::Ar:
return "Archive (GNU ar)";
case ContainerFormat::Asf:
return "Advanced Systems Format";
case ContainerFormat::Elf:
@ -337,6 +344,8 @@ const char *containerFormatName(ContainerFormat containerFormat)
return "RIFF/WAVE";
case ContainerFormat::RiffAvi:
return "RIFF/Audio Video Interleave";
case ContainerFormat::Tar:
return "TAR archive";
case ContainerFormat::TiffBigEndian:
case ContainerFormat::TiffLittleEndian:
return "Tagged Image File Format";
@ -356,10 +365,10 @@ const char *containerFormatName(ContainerFormat containerFormat)
return "gzip compressed file";
case ContainerFormat::Lzip:
return "lzip compressed file";
case ContainerFormat::Zip:
return "ZIP archive";
case ContainerFormat::SevenZ:
return "7z archive";
case ContainerFormat::Zip:
return "ZIP archive";
default:
return "unknown";
}

View File

@ -15,6 +15,7 @@ enum class ContainerFormat
{
Unknown, /**< unknown container format */
Adts, /** < Audio Data Transport Stream */
Ar, /** < "GNU ar" archive */
Asf, /**< Advanced Systems Format */
Bzip2, /** bzip2 compressed file */
Elf, /**< Executable and Linkable Format */
@ -39,6 +40,7 @@ enum class ContainerFormat
Riff, /**< Resource Interchange File Format */
RiffWave, /**< WAVE (subset of RIFF) */
RiffAvi, /**< Audio Video Interleave (subset of RIFF) */
Tar, /** < Tar archive */
TiffBigEndian, /**< Tagged Image File Format (big endian) */
TiffLittleEndian, /**< Tagged Image File Format (little endian) */
Utf16Text, /**< UTF-16 text */

View File

@ -4,7 +4,7 @@ appname = "Tag Parser"
appauthor = Martchus
appurl = "https://github.com/$${appauthor}/$${projectname}"
QMAKE_TARGET_DESCRIPTION = "C++ library for reading and writing MP4 (iTunes), ID3, Vorbis and Matroska tags."
VERSION = 4.0.2
VERSION = 5.0.0
# include ../../common.pri when building as part of a subdirs project; otherwise include general.pri
!include(../../common.pri) {
@ -30,6 +30,9 @@ forcefullparsedefault {
HEADERS += \
abstractcontainer.h \
abstracttrack.h \
aspectratio.h \
avc/avcinfo.h \
avc/avcconfiguration.h \
backuphelper.h \
basicfileinfo.h \
exceptions.h \
@ -78,6 +81,7 @@ HEADERS += \
ogg/oggcontainer.h \
vorbis/vorbispackagetypes.h \
vorbis/vorbisidentificationheader.h \
opus/opusidentificationheader.h \
ogg/oggiterator.h \
vorbis/vorbiscommentids.h \
abstractchapter.h \
@ -97,6 +101,9 @@ HEADERS += \
SOURCES += \
abstractcontainer.cpp \
abstracttrack.cpp \
aspectratio.cpp \
avc/avcinfo.cpp \
avc/avcconfiguration.cpp
backuphelper.cpp \
basicfileinfo.cpp \
exceptions.cpp \
@ -136,6 +143,7 @@ SOURCES += \
ogg/oggstream.cpp \
ogg/oggcontainer.cpp \
vorbis/vorbisidentificationheader.cpp \
opus/opusidentificationheader.cpp \
ogg/oggiterator.cpp \
vorbis/vorbiscommentids.cpp \
abstractchapter.cpp \
@ -152,15 +160,11 @@ SOURCES += \
underconstruction {
HEADERS += \
aac/aacframe.h \
aac/aaccodebook.h \
avc/avcinfo.h \
avc/avcconfiguration.h
aac/aaccodebook.h
SOURCES += \
aac/aacframe.cpp \
aac/aaccodebook.cpp \
avc/avcinfo.cpp \
avc/avcconfiguration.cpp
aac/aaccodebook.cpp
}
OTHER_FILES += \

1
tests/cppunit.cpp Normal file
View File

@ -0,0 +1 @@
#include <c++utilities/tests/cppunit.h>

51
tests/matroska.cpp Normal file
View File

@ -0,0 +1,51 @@
#include <c++utilities/tests/testutils.h>
#include <cppunit/extensions/HelperMacros.h>
#include <cppunit/TestFixture.h>
#include <fstream>
#include <sstream>
#include <iostream>
using namespace std;
using namespace CPPUNIT_NS;
class MatroskaTests : public TestFixture
{
CPPUNIT_TEST_SUITE(MatroskaTests);
CPPUNIT_TEST(testParsing);
CPPUNIT_TEST(testMaking);
CPPUNIT_TEST_SUITE_END();
public:
void setUp();
void tearDown();
void testParsing();
void testMaking();
};
CPPUNIT_TEST_SUITE_REGISTRATION(MatroskaTests);
void MatroskaTests::setUp()
{}
void MatroskaTests::tearDown()
{}
/*!
* \brief Tests the Matroska parser via MediaFileInfo.
*/
void MatroskaTests::testParsing()
{
cerr << TestUtilities::workingCopyPath("matroska/test1.mkv") << endl;
}
/*!
* \brief Tests the Matroska maker via MediaFileInfo.
*/
void MatroskaTests::testMaking()
{
}

View File

@ -64,6 +64,7 @@ string VorbisComment::fieldId(KnownField field) const
case KnownField::Description: return description();
case KnownField::RecordLabel: return label();
case KnownField::Performers: return performer();
case KnownField::Language: return language();
case KnownField::Lyricist: return lyricist();
default: return string();
}
@ -105,7 +106,7 @@ KnownField VorbisComment::knownField(const string &id) const
* \throws Throws Media::Failure or a derived exception when a parsing
* error occurs.
*/
void VorbisComment::parse(OggIterator &iterator)
void VorbisComment::parse(OggIterator &iterator, bool skipSignature)
{
// prepare parsing
invalidateStatus();
@ -115,15 +116,23 @@ void VorbisComment::parse(OggIterator &iterator)
try {
// read signature: 0x3 + "vorbis"
char sig[8];
iterator.read(sig, 7);
if((ConversionUtilities::BE::toUInt64(sig) & 0xffffffffffffff00u) == 0x03766F7262697300u) {
if(!skipSignature) {
iterator.read(sig, 7);
skipSignature = (ConversionUtilities::BE::toUInt64(sig) & 0xffffffffffffff00u) == 0x03766F7262697300u;
}
if(skipSignature) {
// read vendor (length prefixed string)
{
iterator.read(sig, 4);
auto vendorSize = LE::toUInt32(sig);
auto buff = make_unique<char []>(vendorSize);
iterator.read(buff.get(), vendorSize);
m_vendor = string(buff.get(), vendorSize);
if(iterator.currentCharacterOffset() + vendorSize <= iterator.streamSize()) {
auto buff = make_unique<char []>(vendorSize);
iterator.read(buff.get(), vendorSize);
m_vendor = string(buff.get(), vendorSize);
} else {
addNotification(NotificationType::Critical, "Vendor information is truncated.", context);
throw TruncatedDataException();
}
}
// read field count
iterator.read(sig, 4);
@ -135,10 +144,10 @@ void VorbisComment::parse(OggIterator &iterator)
try {
field.parse(iterator);
fields().insert(pair<fieldType::identifierType, fieldType>(fieldId, field));
} catch(TruncatedDataException &) {
} catch(const TruncatedDataException &) {
addNotifications(field);
throw;
} catch(Failure &) {
} catch(const Failure &) {
// nothing to do here since notifications will be added anyways
}
addNotifications(field);
@ -150,7 +159,7 @@ void VorbisComment::parse(OggIterator &iterator)
addNotification(NotificationType::Critical, "Signature is invalid.", context);
throw InvalidDataException();
}
} catch(TruncatedDataException &) {
} catch(const TruncatedDataException &) {
m_size = static_cast<uint32>(static_cast<uint64>(iterator.currentCharacterOffset()) - startOffset);
addNotification(NotificationType::Critical, "Vorbis comment is truncated.", context);
throw;
@ -164,7 +173,7 @@ void VorbisComment::parse(OggIterator &iterator)
* \throws Throws Media::Failure or a derived exception when a making
* error occurs.
*/
void VorbisComment::make(std::ostream &stream)
void VorbisComment::make(std::ostream &stream, bool noSignature)
{
// prepare making
invalidateStatus();
@ -176,9 +185,11 @@ void VorbisComment::make(std::ostream &stream)
addNotification(NotificationType::Warning, "Can not convert the assigned vendor to string.", context);
}
BinaryWriter writer(&stream);
// write signature
static const char sig[7] = {0x03, 0x76, 0x6F, 0x72, 0x62, 0x69, 0x73};
stream.write(sig, sizeof(sig));
if(!noSignature) {
// write signature
static const char sig[7] = {0x03, 0x76, 0x6F, 0x72, 0x62, 0x69, 0x73};
stream.write(sig, sizeof(sig));
}
// write vendor
writer.writeUInt32LE(vendor.size());
writer.writeString(vendor);

View File

@ -25,8 +25,8 @@ public:
std::string fieldId(KnownField field) const;
KnownField knownField(const std::string &id) const;
void parse(OggIterator &iterator);
void make(std::ostream &stream);
void parse(OggIterator &iterator, bool skipSignature = false);
void make(std::ostream &stream, bool noSignature = false);
const TagValue &vendor() const;
void setVendor(const TagValue &vendor);

View File

@ -55,46 +55,51 @@ void VorbisCommentField::parse(OggIterator &iterator)
char buff[4];
iterator.read(buff, 4);
if(auto size = LE::toUInt32(buff)) { // read size
// read data
auto data = make_unique<char []>(size);
iterator.read(data.get(), size);
uint32 idSize = 0;
for(const char *i = data.get(), *end = data.get() + size; i != end && *i != '='; ++i, ++idSize);
// extract id
setId(string(data.get(), idSize));
if(!idSize) {
// empty field ID
addNotification(NotificationType::Critical, "The field ID is empty.", context);
throw InvalidDataException();
} else if(id() == VorbisCommentIds::cover()) {
// extract cover value
try {
auto decoded = decodeBase64(data.get() + idSize + 1, size - idSize - 1);
stringstream ss(ios_base::in | ios_base::out | ios_base::binary);
ss.exceptions(ios_base::failbit | ios_base::badbit);
ss.rdbuf()->pubsetbuf(reinterpret_cast<char *>(decoded.first.get()), decoded.second);
BinaryReader reader(&ss);
setTypeInfo(reader.readUInt32BE());
auto size = reader.readUInt32BE();
value().setMimeType(reader.readString(size));
size = reader.readUInt32BE();
value().setDescription(reader.readString(size));
// skip width, height, color depth, number of colors used
ss.seekg(4 * 4, ios_base::cur);
size = reader.readUInt32BE();
auto data = make_unique<char[]>(size);
ss.read(data.get(), size);
value().assignData(move(data), size, TagDataType::Picture);
} catch (const ios_base::failure &) {
addNotification(NotificationType::Critical, "An IO error occured when reading the METADATA_BLOCK_PICTURE struct.", context);
throw Failure();
} catch (const ConversionException &) {
addNotification(NotificationType::Critical, "Base64 data from METADATA_BLOCK_PICTURE is invalid.", context);
if(iterator.currentCharacterOffset() + size <= iterator.streamSize()) {
// read data
auto data = make_unique<char []>(size);
iterator.read(data.get(), size);
uint32 idSize = 0;
for(const char *i = data.get(), *end = data.get() + size; i != end && *i != '='; ++i, ++idSize);
// extract id
setId(string(data.get(), idSize));
if(!idSize) {
// empty field ID
addNotification(NotificationType::Critical, "The field ID is empty.", context);
throw InvalidDataException();
} else if(id() == VorbisCommentIds::cover()) {
// extract cover value
try {
auto decoded = decodeBase64(data.get() + idSize + 1, size - idSize - 1);
stringstream ss(ios_base::in | ios_base::out | ios_base::binary);
ss.exceptions(ios_base::failbit | ios_base::badbit);
ss.rdbuf()->pubsetbuf(reinterpret_cast<char *>(decoded.first.get()), decoded.second);
BinaryReader reader(&ss);
setTypeInfo(reader.readUInt32BE());
auto size = reader.readUInt32BE();
value().setMimeType(reader.readString(size));
size = reader.readUInt32BE();
value().setDescription(reader.readString(size));
// skip width, height, color depth, number of colors used
ss.seekg(4 * 4, ios_base::cur);
size = reader.readUInt32BE();
auto data = make_unique<char[]>(size);
ss.read(data.get(), size);
value().assignData(move(data), size, TagDataType::Picture);
} catch (const ios_base::failure &) {
addNotification(NotificationType::Critical, "An IO error occured when reading the METADATA_BLOCK_PICTURE struct.", context);
throw Failure();
} catch (const ConversionException &) {
addNotification(NotificationType::Critical, "Base64 data from METADATA_BLOCK_PICTURE is invalid.", context);
throw InvalidDataException();
}
} else if(id().size() + 1 < size) {
// extract other values (as string)
setValue(string(data.get() + idSize + 1, size - idSize - 1));
}
} else if(id().size() + 1 < size) {
// extract other values (as string)
setValue(string(data.get() + idSize + 1, size - idSize - 1));
} else {
addNotification(NotificationType::Critical, "Field is truncated.", context);
throw TruncatedDataException();
}
}
}

View File

@ -40,6 +40,9 @@ inline LIB_EXPORT const char *label() {
inline LIB_EXPORT const char *labelNo() {
return "LABELNO";
}
inline LIB_EXPORT const char *language() {
return "LANGUAGE";
}
inline LIB_EXPORT const char *performer() {
return "PERFORMER";
}