diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml index 54d4d091..284c16b7 100644 --- a/.github/workflows/c-cpp.yml +++ b/.github/workflows/c-cpp.yml @@ -11,7 +11,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: configure - run: mkdir build && cd build && cmake -DCMAKE_BUILD_TYPE=Release .. + run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release - name: build run: cmake --build build - name: test @@ -22,7 +22,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: configure - run: mkdir build && cd build && cmake .. + run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release - name: build run: cmake --build build - name: test @@ -33,7 +33,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: configure - run: mkdir build && cd build && cmake .. + run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug - name: build run: cmake --build build --config Debug - name: test diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml index 8ba6dac8..03f11a47 100644 --- a/.github/workflows/clang-format.yml +++ b/.github/workflows/clang-format.yml @@ -6,9 +6,21 @@ jobs: name: Formatting Check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Run clang-format style check for C/C++ programs. - uses: jidicula/clang-format-action@v4.11.0 - with: - clang-format-version: '15' - check-path: 'IsoLib/libisomediafile' + - uses: actions/checkout@v4 + + - name: Install clang-format + run: | + wget https://apt.llvm.org/llvm.sh + chmod +x llvm.sh + sudo ./llvm.sh 18 + sudo apt-get install -y clang-format-18 + + - name: Run clang-format check on IsoLib/libisomediafile + run: | + find IsoLib/libisomediafile \( -name "*.h" -o -name "*.cpp" -o -name "*.c" \) | \ + xargs clang-format-18 --dry-run --Werror -style=file + + - name: Run clang-format check on IsoLib/t35_tool + run: | + find IsoLib/t35_tool \( -name "*.h" -o -name "*.cpp" -o -name "*.c" \) | \ + xargs clang-format-18 --dry-run --Werror -style=file diff --git a/.gitignore b/.gitignore index d408d56b..08e657b7 100644 --- a/.gitignore +++ b/.gitignore @@ -82,3 +82,7 @@ doc/ *.code-workspace # Local History for Visual Studio Code .history/ + +# T35 Tool Test Output +TestData/t35_tool/output_all_modes/ +TestData/t35_tool/output_smpte/ diff --git a/CMakeLists.txt b/CMakeLists.txt index a58ff119..237e47d5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,5 +50,6 @@ if(NOT ISOBMFF_BUILD_LIB_ONLY) add_subdirectory(IsoLib/isoiff_tool) add_subdirectory(IsoLib/pcm_audio_example) add_subdirectory(IsoLib/vvc_base) + add_subdirectory(IsoLib/t35_tool) add_subdirectory(test) endif() diff --git a/IsoLib/favs_example/src/hevc.c b/IsoLib/favs_example/src/hevc.c index 25e2c904..9ad04689 100644 --- a/IsoLib/favs_example/src/hevc.c +++ b/IsoLib/favs_example/src/hevc.c @@ -756,7 +756,7 @@ u8* stripNALEmulation(u8* buffer, u32* bufferLen) { return outBuffer; } -int parseHEVCNal(FILE* input, u8** data, int* data_len) { +u32 parseHEVCNal(FILE* input, u8** data, u32* data_len) { size_t startPos; size_t NALStart = 0; size_t NALEnd = 0; diff --git a/IsoLib/favs_example/src/hevc.h b/IsoLib/favs_example/src/hevc.h index e573533e..0283a42f 100644 --- a/IsoLib/favs_example/src/hevc.h +++ b/IsoLib/favs_example/src/hevc.h @@ -13,7 +13,7 @@ MP4Err hevc_parse_pps_minimal(BitBuffer *bb, struct hevc_pps* pps); MP4Err hevc_parse_slice_header_minimal(BitBuffer *bb, struct hevc_poc* poc, struct hevc_slice_header* header, struct hevc_sps* sps, struct hevc_pps* pps); u8* stripNALEmulation(u8* buffer, u32* bufferLen); -int parseHEVCNal(FILE* input, u8** data, int* data_len); +u32 parseHEVCNal(FILE* input, u8** data, u32* data_len); ISOErr analyze_hevc_stream(FILE* input, struct hevc_stream* stream); #endif diff --git a/IsoLib/ipmp_example/src/protectAudioMovie.c b/IsoLib/ipmp_example/src/protectAudioMovie.c index 5996d00d..700b283a 100644 --- a/IsoLib/ipmp_example/src/protectAudioMovie.c +++ b/IsoLib/ipmp_example/src/protectAudioMovie.c @@ -70,7 +70,7 @@ MP4Err addIPMPToolDescriptorUpdateToODAccessUnit( MP4Handle odAccessUnitH, / / main function / -/*=====*/ +/ =====*/ int main( int argc, char **argv ) { @@ -100,7 +100,7 @@ int main( int argc, char **argv ) / protectMyAudioMovie / Protect the audio media track using an IPMP Tool specified by its IPMP_ToolID / -/*=====*/ +/ =====*/ MP4Err protectMyAudioMovie( char *inFilename, char *outFilename, u64 ipmpToolID ) { @@ -355,7 +355,7 @@ MP4Err protectMyAudioMovie( char *inFilename, char *outFilename, u64 ipmpToolID / associateSampleDescWithIPMPToolPtr / Create the IPMP_ToolDecriptorPointer and add it to the media Sample Description / -/*=====*/ +/ =====*/ MP4Err associateSampleDescWithIPMPToolPtr( MP4Handle outMediaSampleDescrH, u16 theIPMP_ToolDescriptorID) { @@ -384,7 +384,7 @@ MP4Err associateSampleDescWithIPMPToolPtr( MP4Handle outMediaSampleDescrH, / addIPMPToolDescriptorUpdateToODAccessUnit / Create the IPMP_ToolDecriptorUpdate command and add it to an OD access unit / -/*=====*/ +/ =====*/ MP4Err addIPMPToolDescriptorUpdateToODAccessUnit( MP4Handle odAccessUnitH, u16 ipmpToolDescriptorId, @@ -437,7 +437,7 @@ MP4Err addIPMPToolDescriptorUpdateToODAccessUnit( MP4Handle odAccessUnitH, / protectSample / Protect one media access unit (or sample) / -/*=====*/ +/ =====*/ MP4Err protectSample(MP4Handle sampleH, u64 ipmpToolID) { @@ -452,7 +452,7 @@ MP4Err protectSample(MP4Handle sampleH, u64 ipmpToolID) { / putToolListInIOD / Create the IPMP Tool List and put it in the IOD / -/*=====*/ +/ =====*/ MP4Err putToolListInIOD( MP4Movie theMovie, u64 theIpmpToolID) { diff --git a/IsoLib/isoiff_tool/src/HEVCDecoderConfigRecord.c b/IsoLib/isoiff_tool/src/HEVCDecoderConfigRecord.c index b6560b7d..5e2d9327 100644 --- a/IsoLib/isoiff_tool/src/HEVCDecoderConfigRecord.c +++ b/IsoLib/isoiff_tool/src/HEVCDecoderConfigRecord.c @@ -334,13 +334,13 @@ MP4Err ISOIFF_CreateHEVCDecConfRecFromHandle(MP4Handle recordDataHandle, // buffer += 4; memcpy(&tmp8, buffer, 1); buffer += 1; - hevcDecConfRec->general_profile_compatibility_flags = (tmp8 << 24); + hevcDecConfRec->general_profile_compatibility_flags = ((u64)tmp8 << 24); memcpy(&tmp8, buffer, 1); buffer += 1; - hevcDecConfRec->general_profile_compatibility_flags |= (tmp8 << 16); + hevcDecConfRec->general_profile_compatibility_flags |= ((u64)tmp8 << 16); memcpy(&tmp8, buffer, 1); buffer += 1; - hevcDecConfRec->general_profile_compatibility_flags |= (tmp8 << 8); + hevcDecConfRec->general_profile_compatibility_flags |= ((u64)tmp8 << 8); memcpy(&tmp8, buffer, 1); buffer += 1; hevcDecConfRec->general_profile_compatibility_flags |= tmp8; @@ -350,19 +350,19 @@ MP4Err ISOIFF_CreateHEVCDecConfRecFromHandle(MP4Handle recordDataHandle, // hevcDecConfRec->general_constraint_indicator_flags = tmp64 >> 16; memcpy(&tmp8, buffer, 1); buffer += 1; - hevcDecConfRec->general_constraint_indicator_flags = tmp8 << 40; + hevcDecConfRec->general_constraint_indicator_flags = (u64)tmp8 << 40; memcpy(&tmp8, buffer, 1); buffer += 1; - hevcDecConfRec->general_constraint_indicator_flags |= (tmp8 << 32); + hevcDecConfRec->general_constraint_indicator_flags |= ((u64)tmp8 << 32); memcpy(&tmp8, buffer, 1); buffer += 1; - hevcDecConfRec->general_constraint_indicator_flags |= (tmp8 << 24); + hevcDecConfRec->general_constraint_indicator_flags |= ((u64)tmp8 << 24); memcpy(&tmp8, buffer, 1); buffer += 1; - hevcDecConfRec->general_constraint_indicator_flags |= (tmp8 << 16); + hevcDecConfRec->general_constraint_indicator_flags |= ((u64)tmp8 << 16); memcpy(&tmp8, buffer, 1); buffer += 1; - hevcDecConfRec->general_constraint_indicator_flags |= (tmp8 << 8); + hevcDecConfRec->general_constraint_indicator_flags |= ((u64)tmp8 << 8); memcpy(&tmp8, buffer, 1); buffer += 1; hevcDecConfRec->general_constraint_indicator_flags |= tmp8; diff --git a/IsoLib/libisomediafile/CMakeLists.txt b/IsoLib/libisomediafile/CMakeLists.txt index 11ea23a9..27eb2c05 100644 --- a/IsoLib/libisomediafile/CMakeLists.txt +++ b/IsoLib/libisomediafile/CMakeLists.txt @@ -103,6 +103,7 @@ add_library( src/MetaboxRelationAtom.c src/MJ2BitsPerComponentAtom.c src/MJ2ColorSpecificationAtom.c + src/MP4ColourInformationAtom.c src/MJ2FileTypeAtom.c src/MJ2HeaderAtom.c src/MJ2ImageHeaderAtom.c @@ -186,6 +187,7 @@ add_library( src/SubSampleInformationAtom.c src/SubsegmentIndexAtom.c src/SyncSampleAtom.c + src/T35MetadataSampleEntry.c src/TextMetaSampleEntry.c src/TimeToSampleAtom.c src/TrackAtom.c diff --git a/IsoLib/libisomediafile/src/ISOMovies.h b/IsoLib/libisomediafile/src/ISOMovies.h index 24ea3997..30c2a743 100644 --- a/IsoLib/libisomediafile/src/ISOMovies.h +++ b/IsoLib/libisomediafile/src/ISOMovies.h @@ -63,6 +63,7 @@ extern "C" #define ISOOpenMovieInPlace MP4OpenMovieInPlace struct MP4BoxedMetadataSampleEntry; + struct MP4T35MetadataSampleEntry; /** * @brief constants for the graphics modes (e.g. for MJ2SetMediaGraphicsMode) @@ -311,7 +312,8 @@ extern "C" #define ISOGetUserDataTypeCount MP4GetUserDataTypeCount #define ISONewUserData MP4NewUserData #define ISOCreateTrackReader MP4CreateTrackReader -#define ISOSetMebxTrackReader MP4SetMebxTrackReader +#define ISOSetMebxTrackReaderLocalKeyId MP4SetMebxTrackReaderLocalKeyId +#define ISOSelectFirstMebxTrackReaderKey MP4SelectFirstMebxTrackReaderKey #define ISODisposeTrackReader MP4DisposeTrackReader #define ISONewHandle MP4NewHandle #define ISOSetHandleSize MP4SetHandleSize @@ -800,13 +802,96 @@ extern "C" * @param sampleEntryH input sample entry of the mebx track * @param key_cnt number of local_key_id's */ - ISO_EXTERN(ISOErr) - ISOGetMebxMetadataCount(MP4Handle sampleEntryH, u32 *key_cnt); + ISO_EXTERN(ISOErr) ISOGetMebxMetadataCount(MP4Handle sampleEntryH, u32 *key_cnt); + /** + * @brief Get metadata key configuration from a 'mebx' sample entry. + * + * Retrieves the key information at index @p idx from the MetadataKeyTableBox. Returns namespace, + * value, locale, setup data, and the local_key_id for this entry. + * + * @param sampleEntryH Handle containing the 'mebx' sample entry. + * @param idx Zero-based index of the key entry to query. + * @param local_key_id Output; receives the local_key_id for this key. + * @param key_namespace Output; receives the namespace FourCC. + * @param key_value Optional handle to receive the key value data. + * @param locale_string Optional; receives locale string if present. + * @param setupInfo Optional handle to receive setup information if present. + * + * @return ISOErr code: MP4NoErr on success, MP4BadDataErr if no key table, MP4NotFoundErr if not + * found, or other error codes. + */ ISO_EXTERN(ISOErr) - ISOGetMebxMetadataConfig(MP4Handle sampleEntryH, u32 cnt, u32 *local_key_id, u32 *key_namespace, + ISOGetMebxMetadataConfig(MP4Handle sampleEntryH, u32 idx, u32 *local_key_id, u32 *key_namespace, MP4Handle key_value, char **locale_string, MP4Handle setupInfo); + /************************************************************************************************* + * T.35 Metadata Track Functions + ************************************************************************************************/ + + /** + * @brief Create a new T.35 metadata sample entry. + * @ingroup SampleDescr + * + * Creates a T35MetadataSampleEntry ('it35') with a T35CommonHeaderBox ('t35C') containing + * the specified T.35 prefix text. + * + * @param outSE Output; receives the created T35MetadataSampleEntry. + * @param dataReferenceIndex Data reference index (typically 1 for self-contained media). + * @param t35_prefix_text UTF-8 string conforming to format: T35Prefix[:T35Description] + * where T35Prefix is even number of uppercase hex digits (0-9, A-F). + * Example: "B500900001:SMPTE-ST2094-50" + * @return MP4Err code: MP4NoErr on success, MP4BadParamErr if validation fails. + */ + MP4_EXTERN(MP4Err) + ISONewT35SampleDescription(struct MP4T35MetadataSampleEntry **outSE, u32 dataReferenceIndex, + const char *t35_prefix_text); + + /** + * @brief Create a complete T.35 timed metadata track. + * @ingroup Tracks + * + * Convenience function that creates a metadata track with MP4MetaHandlerType, + * adds a T35MetadataSampleEntry with the specified T.35 prefix, and optionally + * adds a track reference to a video track using the 'rndr' reference type. + * + * After calling this function, use MP4AddMediaSample() or similar functions to + * add T.35 metadata samples to the track. + * + * @param theMovie Input movie object. + * @param timescale Media timescale (typically matches video track timescale). + * @param t35_prefix_text UTF-8 T.35 prefix string (e.g., "B500900001:SMPTE-ST2094-50"). + * @param videoTrack Optional video track for track reference (NULL if not needed). + * @param trackReferenceType Track reference type or 0 for no reference. + * @param outTrack Output; receives the created metadata track. + * @param outMedia Optional output; receives the created media (NULL if not needed). + * @return ISOErr code: MP4NoErr on success, error code otherwise. + */ + ISO_EXTERN(ISOErr) + ISONewT35MetadataTrack(MP4Movie theMovie, u32 timescale, const char *t35_prefix_text, + MP4Track videoTrack, u32 trackReferenceType, MP4Track *outTrack, + MP4Media *outMedia); + + /** + * @brief Read the t35_identifier and description from a serialized T.35 sample entry handle. + * @ingroup SampleDescr + * + * Properly deserializes the handle returned by MP4GetMediaSampleDescription() and extracts + * the T.35 identifier bytes and optional description string. The caller is responsible for + * freeing *outIdentifier and *outDescription with free(). + * + * @param sampleEntryH Handle containing the serialized 'it35' sample entry. + * @param outIdentifier Output; receives a newly allocated copy of the t35_identifier bytes. + * @param outIdentifierSize Output; receives the number of bytes in *outIdentifier. + * @param outDescription Optional output; receives a newly allocated description string, or NULL + * if no description is present. Pass NULL to ignore. + * @return MP4NoErr on success, MP4NotFoundErr if no identifier is present, MP4BadParamErr on + * invalid input. + */ + ISO_EXTERN(ISOErr) + ISOGetT35SampleEntryFields(MP4Handle sampleEntryH, u8 **outIdentifier, u32 *outIdentifierSize, + char **outDescription); + /************************************************************************************************* * VVC Sample descriptions ************************************************************************************************/ diff --git a/IsoLib/libisomediafile/src/ISOSampleDescriptions.c b/IsoLib/libisomediafile/src/ISOSampleDescriptions.c index 85489a9f..5c3d1f0e 100644 --- a/IsoLib/libisomediafile/src/ISOSampleDescriptions.c +++ b/IsoLib/libisomediafile/src/ISOSampleDescriptions.c @@ -1211,17 +1211,6 @@ ISOGetHEVCNALUs(MP4Handle sampleEntryH, MP4Handle nalus, u32 extraction_mode) err = sampleEntryHToAtomPtr(sampleEntryH, (MP4AtomPtr *)&entry, MP4VisualSampleEntryAtomType); if(err) goto bail; - if(entry->type == MP4EncVisualSampleEntryAtomType || - entry->type == MP4RestrictedVideoSampleEntryAtomType) - { - u32 origFmt = 0; - err = ISOGetOriginalFormat(sampleEntryH, &origFmt); - if(origFmt != ISOHEVCSampleEntryAtomType && origFmt != ISOLHEVCSampleEntryAtomType) - BAILWITHERROR(MP4BadParamErr); - } - else if(entry->type != ISOHEVCSampleEntryAtomType && entry->type != ISOLHEVCSampleEntryAtomType) - BAILWITHERROR(MP4BadParamErr); - MP4GetListEntryAtom(entry->ExtensionAtomList, ISOHEVCConfigAtomType, (MP4AtomPtr *)&hvcC); MP4GetListEntryAtom(entry->ExtensionAtomList, ISOLHEVCConfigAtomType, (MP4AtomPtr *)&lhvC); @@ -1882,7 +1871,7 @@ ISOAddMebxMetadataToSampleEntry(MP4BoxedMetadataSampleEntryPtr mebx, u32 desired if(err) goto bail; } - keytable->addMetaDataKeyBox(keytable, (MP4AtomPtr)keyb); + err = keytable->addMetaDataKeyBox(keytable, (MP4AtomPtr)keyb); if(err) goto bail; bail: @@ -1920,7 +1909,7 @@ ISOGetMebxMetadataCount(MP4Handle sampleEntryH, u32 *key_cnt) } ISO_EXTERN(ISOErr) -ISOGetMebxMetadataConfig(MP4Handle sampleEntryH, u32 cnt, u32 *local_key_id, u32 *key_namespace, +ISOGetMebxMetadataConfig(MP4Handle sampleEntryH, u32 idx, u32 *local_key_id, u32 *key_namespace, MP4Handle key_value, char **locale_string, MP4Handle setupInfo) { MP4Err err; @@ -1933,7 +1922,7 @@ ISOGetMebxMetadataConfig(MP4Handle sampleEntryH, u32 cnt, u32 *local_key_id, u32 if(entry->keyTable == NULL) BAILWITHERROR(MP4BadDataErr); - err = MP4GetListEntry(entry->keyTable->metadataKeyBoxList, cnt, (char **)&key); + err = MP4GetListEntry(entry->keyTable->metadataKeyBoxList, idx, (char **)&key); if(err) goto bail; /* set output values */ @@ -2419,3 +2408,239 @@ ISOGetVVCSubpicSampleDescription(MP4Handle sampleEntryH, u32 *dataReferenceIndex if(entry) entry->destroy((MP4AtomPtr)entry); return err; } + +/* ==================== T.35 Metadata Track Functions ==================== */ + +/* Helper: Parse T.35 prefix string into hex identifier and description + * Format: "HEXSTRING:Description" + * Example: "B500900001:SMPTE-ST2094-50" + */ +static MP4Err parseT35PrefixString(const char *t35_prefix_text, u8 **outIdentifier, + u32 *outIdentifierSize, char **outDescription) +{ + MP4Err err; + const char *colon; + const char *hexStart; + size_t hexLen; + u32 identifierSize; + u8 *identifier; + char *description; + + if(t35_prefix_text == NULL || outIdentifier == NULL || outIdentifierSize == NULL || + outDescription == NULL) + BAILWITHERROR(MP4BadParamErr); + + /* Find colon separator */ + colon = strchr(t35_prefix_text, ':'); + if(colon) + { + hexLen = colon - t35_prefix_text; + } + else + { + hexLen = strlen(t35_prefix_text); + } + + /* Check if hex length is even */ + if(hexLen % 2 != 0) BAILWITHERROR(MP4BadParamErr); + + identifierSize = (u32)(hexLen / 2); + if(identifierSize == 0) BAILWITHERROR(MP4BadParamErr); + + /* Allocate identifier buffer */ + identifier = (u8 *)calloc(identifierSize, 1); + if(identifier == NULL) BAILWITHERROR(MP4NoMemoryErr); + + /* Parse hex string */ + hexStart = t35_prefix_text; + for(u32 i = 0; i < identifierSize; i++) + { + char hexByte[3]; + hexByte[0] = hexStart[i * 2]; + hexByte[1] = hexStart[i * 2 + 1]; + hexByte[2] = '\0'; + + char *endPtr; + unsigned long value = strtoul(hexByte, &endPtr, 16); + if(*endPtr != '\0' || value > 255) + { + free(identifier); + BAILWITHERROR(MP4BadParamErr); + } + identifier[i] = (u8)value; + } + + /* Parse description (after colon) */ + if(colon && colon[1] != '\0') + { + size_t descLen = strlen(colon + 1); + description = (char *)calloc(descLen + 1, 1); + if(description == NULL) + { + free(identifier); + BAILWITHERROR(MP4NoMemoryErr); + } + strcpy(description, colon + 1); + } + else + { + /* Empty description */ + description = NULL; + } + + *outIdentifier = identifier; + *outIdentifierSize = identifierSize; + *outDescription = description; + + return MP4NoErr; + +bail: + TEST_RETURN(err); + return err; +} + +MP4_EXTERN(MP4Err) +ISONewT35SampleDescription(MP4T35MetadataSampleEntryPtr *outSE, u32 dataReferenceIndex, + const char *t35_prefix_text) +{ + MP4Err err; + MP4T35MetadataSampleEntryPtr it35; + u8 *identifier = NULL; + u32 identifierSize = 0; + char *description = NULL; + + if(outSE == NULL || t35_prefix_text == NULL) BAILWITHERROR(MP4BadParamErr); + + /* Parse t35_prefix_text into identifier and description */ + err = parseT35PrefixString(t35_prefix_text, &identifier, &identifierSize, &description); + if(err) goto bail; + + /* Create T35 sample entry */ + err = MP4CreateT35MetadataSampleEntry(&it35); + if(err) goto bail; + it35->dataReferenceIndex = dataReferenceIndex; + + /* Set description and t35_identifier fields */ + it35->description = description; + it35->t35_identifier = identifier; + it35->t35_identifier_size = identifierSize; + + *outSE = it35; + + return MP4NoErr; + +bail: + if(identifier) free(identifier); + if(description) free(description); + TEST_RETURN(err); + return err; +} + +ISO_EXTERN(ISOErr) +ISONewT35MetadataTrack(MP4Movie theMovie, u32 timescale, const char *t35_prefix_text, + MP4Track videoTrack, u32 trackReferenceType, MP4Track *outTrack, + MP4Media *outMedia) +{ + MP4Err err; + MP4Track trakM = NULL; + MP4Media mediaM = NULL; + MP4T35MetadataSampleEntryPtr it35 = NULL; + MP4Handle sampleEntryH = NULL; + MP4PrivateMovieRecordPtr moov = NULL; + MP4TrackAtomPtr trakAtom = NULL; + + if(theMovie == NULL || t35_prefix_text == NULL || outTrack == NULL) BAILWITHERROR(MP4BadParamErr); + + moov = (MP4PrivateMovieRecordPtr)theMovie; + + /* Create metadata track */ + err = MP4NewMovieTrack(theMovie, MP4NewTrackIsMetadata, &trakM); + if(err) goto bail; + + /* Create media with MP4MetaHandlerType */ + err = MP4NewTrackMedia(trakM, &mediaM, MP4MetaHandlerType, timescale, NULL); + if(err) goto bail; + + /* Add track reference if both videoTrack and trackReferenceType are provided */ + if(videoTrack != NULL && trackReferenceType != 0) + { + err = MP4AddTrackReference(trakM, videoTrack, trackReferenceType, 0); + if(err) goto bail; + } + + /* Create T35 sample entry with description and t35_identifier */ + err = ISONewT35SampleDescription(&it35, 1, t35_prefix_text); + if(err) goto bail; + + /* Convert sample entry to handle */ + err = MP4NewHandle(0, &sampleEntryH); + if(err) goto bail; + + /* Use atomPtrToSampleEntryH helper */ + err = atomPtrToSampleEntryH(sampleEntryH, (MP4AtomPtr)it35); + if(err) goto bail; + + /* Add sample entry to media (index 0 means add to sample description table) */ + err = MP4AddMediaSamples(mediaM, 0, 0, 0, 0, sampleEntryH, 0, 0); + if(err) goto bail; + + /* Dispose the handle after adding */ + MP4DisposeHandle(sampleEntryH); + sampleEntryH = NULL; + + /* Set the mdat reference for the track */ + trakAtom = (MP4TrackAtomPtr)trakM; + if(trakAtom && moov->mdat) + { + err = trakAtom->setMdat(trakAtom, moov->mdat); + if(err) goto bail; + } + + *outTrack = trakM; + if(outMedia) *outMedia = mediaM; + +bail: + if(sampleEntryH) MP4DisposeHandle(sampleEntryH); + if(it35) it35->destroy((MP4AtomPtr)it35); + + TEST_RETURN(err); + return err; +} + +ISO_EXTERN(ISOErr) +ISOGetT35SampleEntryFields(MP4Handle sampleEntryH, u8 **outIdentifier, u32 *outIdentifierSize, + char **outDescription) +{ + MP4Err err; + MP4T35MetadataSampleEntryPtr it35 = NULL; + + if(sampleEntryH == NULL || outIdentifier == NULL || outIdentifierSize == NULL) + BAILWITHERROR(MP4BadParamErr); + + *outIdentifier = NULL; + *outIdentifierSize = 0; + if(outDescription) *outDescription = NULL; + + err = sampleEntryHToAtomPtr(sampleEntryH, (MP4AtomPtr *)&it35, MP4T35MetadataSampleEntryType); + if(err) goto bail; + + if(it35->t35_identifier == NULL || it35->t35_identifier_size == 0) BAILWITHERROR(MP4NotFoundErr); + + *outIdentifier = (u8 *)calloc(it35->t35_identifier_size, 1); + TESTMALLOC(*outIdentifier); + memcpy(*outIdentifier, it35->t35_identifier, it35->t35_identifier_size); + *outIdentifierSize = it35->t35_identifier_size; + + if(outDescription && it35->description && it35->description[0] != '\0') + { + u32 len = (u32)strlen(it35->description) + 1; + *outDescription = (char *)calloc(len, 1); + TESTMALLOC(*outDescription); + memcpy(*outDescription, it35->description, len); + } + +bail: + if(it35) it35->destroy((MP4AtomPtr)it35); + TEST_RETURN(err); + return err; +} diff --git a/IsoLib/libisomediafile/src/MP4Atoms.c b/IsoLib/libisomediafile/src/MP4Atoms.c index 4d1880c6..6d0fd9f4 100644 --- a/IsoLib/libisomediafile/src/MP4Atoms.c +++ b/IsoLib/libisomediafile/src/MP4Atoms.c @@ -424,6 +424,10 @@ MP4Err MP4CreateAtom(u32 atomType, MP4AtomPtr *outAtom) err = MP4CreateExtendedLanguageTagAtom((MP4ExtendedLanguageTagAtomPtr *)&newAtom); break; + case MP4T35MetadataSampleEntryType: + err = MP4CreateT35MetadataSampleEntry((MP4T35MetadataSampleEntryPtr *)&newAtom); + break; + case MP4PaddingBitsAtomType: err = MP4CreatePaddingBitsAtom((MP4PaddingBitsAtomPtr *)&newAtom); break; @@ -445,9 +449,14 @@ MP4Err MP4CreateAtom(u32 atomType, MP4AtomPtr *outAtom) err = MJ2CreateBitsPerComponentAtom((MJ2BitsPerComponentAtomPtr *)&newAtom); break; + /* Remove MJ2 for now case MJ2ColorSpecificationAtomType: err = MJ2CreateColorSpecificationAtom((MJ2ColorSpecificationAtomPtr *)&newAtom); break; + */ + case MP4ColorInformationAtomType: + err = MP4CreateColorInformationAtom((MP4ColorInformationAtomPtr *)&newAtom); + break; case MJ2JP2HeaderAtomType: err = MJ2CreateHeaderAtom((MJ2HeaderAtomPtr *)&newAtom); diff --git a/IsoLib/libisomediafile/src/MP4Atoms.h b/IsoLib/libisomediafile/src/MP4Atoms.h index 1e2d15c3..2ef839d3 100644 --- a/IsoLib/libisomediafile/src/MP4Atoms.h +++ b/IsoLib/libisomediafile/src/MP4Atoms.h @@ -77,6 +77,7 @@ enum MP4ObjectDescriptorAtomType = MP4_FOUR_CHAR_CODE('i', 'o', 'd', 's'), MP4ObjectDescriptorMediaHeaderAtomType = MP4_FOUR_CHAR_CODE('o', 'd', 'h', 'd'), MP4ODTrackReferenceAtomType = MP4_FOUR_CHAR_CODE('m', 'p', 'o', 'd'), + MP4RndrTrackReferenceAtomType = MP4_FOUR_CHAR_CODE('r', 'n', 'd', 'r'), MP4SampleDescriptionAtomType = MP4_FOUR_CHAR_CODE('s', 't', 's', 'd'), MP4SampleSizeAtomType = MP4_FOUR_CHAR_CODE('s', 't', 's', 'z'), MP4CompactSampleSizeAtomType = MP4_FOUR_CHAR_CODE('s', 't', 'z', '2'), @@ -92,6 +93,7 @@ enum MP4SubSampleInformationAtomType = MP4_FOUR_CHAR_CODE('s', 'u', 'b', 's'), MP4SyncSampleAtomType = MP4_FOUR_CHAR_CODE('s', 't', 's', 's'), MP4SyncTrackReferenceAtomType = MP4_FOUR_CHAR_CODE('s', 'y', 'n', 'c'), + MP4T35MetadataSampleEntryType = MP4_FOUR_CHAR_CODE('i', 't', '3', '5'), MP4TimeToSampleAtomType = MP4_FOUR_CHAR_CODE('s', 't', 't', 's'), MP4TrackAtomType = MP4_FOUR_CHAR_CODE('t', 'r', 'a', 'k'), MP4TrackHeaderAtomType = MP4_FOUR_CHAR_CODE('t', 'k', 'h', 'd'), @@ -169,10 +171,21 @@ enum MP4MetadataLocaleBoxType = MP4_FOUR_CHAR_CODE('l', 'o', 'c', 'a'), MP4MetadataSetupBoxType = MP4_FOUR_CHAR_CODE('s', 'e', 't', 'u'), MP4GroupsListBoxType = MP4_FOUR_CHAR_CODE('g', 'r', 'p', 'l'), - MP4AlternativeEntityGroup = MP4_FOUR_CHAR_CODE('a', 'l', 't', 'r') + MP4AlternativeEntityGroup = MP4_FOUR_CHAR_CODE('a', 'l', 't', 'r'), + MP4T35SampleGroupEntry = MP4_FOUR_CHAR_CODE('i', 't', '3', '5'), + MP4ColorInformationAtomType = MP4_FOUR_CHAR_CODE('c', 'o', 'l', 'r') }; +/* Colour Types */ +enum +{ + MP4ColorParameterTypeNCLX = MP4_FOUR_CHAR_CODE('n', 'c', 'l', 'x'), + MP4ColorParameterTypeRICC = MP4_FOUR_CHAR_CODE('r', 'I', 'C', 'C'), + MP4ColorParameterTypePROF = MP4_FOUR_CHAR_CODE('p', 'r', 'o', 'f'), + QTColorParameterTypeNCLC = MP4_FOUR_CHAR_CODE('n', 'c', 'l', 'c') +}; + #ifdef ISMACrypt enum { @@ -851,6 +864,15 @@ typedef struct MP4MPEGSampleEntryAtom COMMON_SAMPLE_ENTRY_FIELDS } MP4MPEGSampleEntryAtom, *MP4MPEGSampleEntryAtomPtr; +typedef struct MP4T35MetadataSampleEntry +{ + MP4_BASE_ATOM + COMMON_SAMPLE_ENTRY_FIELDS + char *description; /* UTF-8 string, '\0' if empty */ + u8 *t35_identifier; /* Variable length byte array */ + u32 t35_identifier_size; /* Size of t35_identifier in bytes */ +} MP4T35MetadataSampleEntry, *MP4T35MetadataSampleEntryPtr; + typedef struct MP4VisualSampleEntryAtom { MP4_BASE_ATOM @@ -936,6 +958,9 @@ typedef struct MP4MetadataKeyTableBox MP4MetadataKeyBoxPtr (*getMetadataKeyBox)(struct MP4MetadataKeyTableBox *self, u32 local_key_id); MP4Err (*addMetaDataKeyBox)(struct MP4MetadataKeyTableBox *self, MP4AtomPtr atom); MP4LinkedList metadataKeyBoxList; + u8 isAppleStyle; /* 1 if Apple QTFF format (FullAtom with entry_count), 0 if mebx format */ + u8 version; + u32 flags; } MP4MetadataKeyTableBox, *MP4MetadataKeyTableBoxPtr; typedef struct MP4BoxedMetadataSampleEntry @@ -2261,6 +2286,19 @@ typedef struct EntityToGroupBox } EntityToGroupBox, *EntityToGroupBoxPtr; +typedef struct MP4ColorInformationAtom +{ + MP4_BASE_ATOM + + u32 colour_type; + u32 colour_primaries; + u32 transfer_characteristics; + u32 matrix_coefficients; + u32 full_range_flag; + char *profile; + u32 profileSize; +} MP4ColorInformationAtom, *MP4ColorInformationAtomPtr; + MP4Err MP4CreateGroupListBox(GroupListBoxPtr *outAtom); MP4Err MP4CreateEntityToGroupBox(EntityToGroupBoxPtr *pOut, u32 type); MP4Err MP4GetListEntryAtom(MP4LinkedList list, u32 atomType, MP4AtomPtr *outItem); @@ -2317,6 +2355,7 @@ MP4Err MP4CreateShadowSyncAtom(MP4ShadowSyncAtomPtr *outAtom); MP4Err MP4CreateSoundMediaHeaderAtom(MP4SoundMediaHeaderAtomPtr *outAtom); MP4Err MP4CreateSubSampleInformationAtom(MP4SubSampleInformationAtomPtr *outAtom); MP4Err MP4CreateSyncSampleAtom(MP4SyncSampleAtomPtr *outAtom); +MP4Err MP4CreateT35MetadataSampleEntry(MP4T35MetadataSampleEntryPtr *outAtom); MP4Err MP4CreateTimeToSampleAtom(MP4TimeToSampleAtomPtr *outAtom); MP4Err MP4CreateTrackAtom(MP4TrackAtomPtr *outAtom); MP4Err MP4CreateTrackHeaderAtom(MP4TrackHeaderAtomPtr *outAtom); @@ -2418,4 +2457,6 @@ MP4Err MP4CreateBitRateAtom(MP4BitRateAtomPtr *outAtom); MP4Err MP4CreateVisualMediaHeaderAtom(MP4VolumetricVisualMediaHeaderAtomPtr *outAtom); +MP4Err MP4CreateColorInformationAtom(MP4ColorInformationAtomPtr *outAtom); + #endif diff --git a/IsoLib/libisomediafile/src/MP4ColourInformationAtom.c b/IsoLib/libisomediafile/src/MP4ColourInformationAtom.c new file mode 100644 index 00000000..5f58c137 --- /dev/null +++ b/IsoLib/libisomediafile/src/MP4ColourInformationAtom.c @@ -0,0 +1,175 @@ +/** + * @file MP4ColourInformationAtom.c + * @brief ISOBMFF Colour Information Box + * @version 0.1 + * + * @copyright This software module was originally developed by Apple Computer, Inc. in the course of + * development of MPEG-4. This software module is an implementation of a part of one or more MPEG-4 + * tools as specified by MPEG-4. ISO/IEC gives users of MPEG-4 free license to this software module + * or modifications thereof for use in hardware or software products claiming conformance to MPEG-4. + * Those intending to use this software module in hardware or software products are advised that its + * use may infringe existing patents. The original developer of this software module and his/her + * company, the subsequent editors and their companies, and ISO/IEC have no liability for use of + * this software module or modifications thereof in an implementation. Copyright is not released for + * non MPEG-4 conforming products. Apple Computer, Inc. retains full right to use the code for its + * own purpose, assign or donate the code to a third party and to inhibit third parties from using + * the code for non MPEG-4 conforming products. This copyright notice must be included in all copies + * or derivative works. Copyright (c) 1999. + * + */ + +#include "MP4Atoms.h" +#include +#include + +static void destroy(MP4AtomPtr s) +{ + MP4ColorInformationAtomPtr self = (MP4ColorInformationAtomPtr)s; + if(self == NULL) return; + if(self->profile) + { + free(self->profile); + self->profile = NULL; + } + if(self->super) self->super->destroy(s); +} + +static ISOErr serialize(struct MP4Atom *s, char *buffer) +{ + ISOErr err; + MP4ColorInformationAtomPtr self = (MP4ColorInformationAtomPtr)s; + + err = ISONoErr; + + err = MP4SerializeCommonBaseAtomFields(s, buffer); + if(err) goto bail; + buffer += self->bytesWritten; + + PUT32(colour_type); + + if(self->colour_type == MP4ColorParameterTypeNCLX) + { + PUT16(colour_primaries); + PUT16(transfer_characteristics); + PUT16(matrix_coefficients); + PUT8(full_range_flag); + } + else if(self->colour_type == QTColorParameterTypeNCLC) + { + PUT16(colour_primaries); + PUT16(transfer_characteristics); + PUT16(matrix_coefficients); + } + else if(self->colour_type == MP4ColorParameterTypeRICC || + self->colour_type == MP4ColorParameterTypePROF) + { + PUTBYTES(self->profile, self->profileSize); + } + + assert(self->bytesWritten == self->size); +bail: + TEST_RETURN(err); + return err; +} + +static ISOErr calculateSize(struct MP4Atom *s) +{ + ISOErr err; + MP4ColorInformationAtomPtr self = (MP4ColorInformationAtomPtr)s; + err = ISONoErr; + + err = MP4CalculateBaseAtomFieldSize(s); + if(err) goto bail; + + self->size += 4; /* colour_type */ + if(self->colour_type == MP4ColorParameterTypeNCLX) + { + self->size += 2; /* colour_primaries */ + self->size += 2; /* transfer_characteristics */ + self->size += 2; /* matrix_coefficients */ + self->size += 1; /* full_range_flag */ + } + else if(self->colour_type == QTColorParameterTypeNCLC) + { + self->size += 2; /* colour_primaries */ + self->size += 2; /* transfer_characteristics */ + self->size += 2; /* matrix_coefficients */ + } + else if(self->colour_type == MP4ColorParameterTypeRICC || + self->colour_type == MP4ColorParameterTypePROF) + { + self->size += self->profileSize; + } + +bail: + TEST_RETURN(err); + return err; +} + +static ISOErr createFromInputStream(MP4AtomPtr s, MP4AtomPtr proto, MP4InputStreamPtr inputStream) +{ + ISOErr err; + MP4ColorInformationAtomPtr self = (MP4ColorInformationAtomPtr)s; + u32 temp; + char typeString[8]; + + err = ISONoErr; + if(self == NULL) BAILWITHERROR(ISOBadParamErr) + + err = self->super->createFromInputStream(s, proto, (char *)inputStream); + if(err) goto bail; + + GET32_V_NOMSG(temp); + MP4TypeToString(temp, typeString); + DEBUG_SPRINTF("colour_type = '%s'", typeString); + self->colour_type = temp; + + if(self->colour_type == MP4ColorParameterTypeNCLX || + self->colour_type == QTColorParameterTypeNCLC) + { + GET16(colour_primaries); + GET16(transfer_characteristics); + GET16(matrix_coefficients); + if(self->colour_type == MP4ColorParameterTypeNCLX) + { + GET8_V_NOMSG(temp); + self->full_range_flag = (temp & 0x80) >> 7; + DEBUG_SPRINTF("full_range_flag = %d", self->full_range_flag); + } + } + else if(self->colour_type == MP4ColorParameterTypeRICC || + self->colour_type == MP4ColorParameterTypePROF) + { + self->profileSize = self->size - self->bytesRead; + self->profile = (char *)malloc(self->profileSize); + TESTMALLOC(self->profile); + GETBYTES(self->profileSize, profile); + } + +bail: + TEST_RETURN(err); + return err; +} + +ISOErr MP4CreateColorInformationAtom(MP4ColorInformationAtomPtr *outAtom) +{ + ISOErr err; + MP4ColorInformationAtomPtr self; + + self = (MP4ColorInformationAtomPtr)calloc(1, sizeof(MP4ColorInformationAtom)); + TESTMALLOC(self); + + err = MP4CreateBaseAtom((MP4AtomPtr)self); + if(err) goto bail; + + self->type = MP4ColorInformationAtomType; + self->name = "ColourInformationBox"; + self->destroy = destroy; + self->createFromInputStream = (cisfunc)createFromInputStream; + self->calculateSize = calculateSize; + self->serialize = serialize; + *outAtom = self; +bail: + TEST_RETURN(err); + return err; +} diff --git a/IsoLib/libisomediafile/src/MP4Media.c b/IsoLib/libisomediafile/src/MP4Media.c index 2e9212fd..822cdf10 100644 --- a/IsoLib/libisomediafile/src/MP4Media.c +++ b/IsoLib/libisomediafile/src/MP4Media.c @@ -177,7 +177,41 @@ ISOAddGroupDescription(MP4Media media, u32 groupType, MP4Handle description, u32 bail: TEST_RETURN(err); + return err; +} + +MP4_EXTERN(MP4Err) +ISOAddT35GroupDescription(MP4Media media, MP4Handle itu_t_t35_data, u32 complete_message_flag, + u32 *index) +{ + MP4Err err; + MP4MediaAtomPtr mdia; + MP4Handle description = NULL; + MP4Handle prefix = NULL; + + if(media == NULL || itu_t_t35_data == NULL) + { + BAILWITHERROR(MP4BadParamErr); + } + mdia = (MP4MediaAtomPtr)media; + + err = MP4NewHandle(1, &prefix); + if(err) goto bail; + (*prefix)[0] = (complete_message_flag ? 0x80 : 0x00); + err = MP4NewHandle(0, &description); + if(err) goto bail; + err = MP4HandleCat(description, prefix); + if(err) goto bail; + err = MP4HandleCat(description, itu_t_t35_data); + if(err) goto bail; + + err = mdia->addGroupDescription(mdia, MP4T35SampleGroupEntry, description, index); + +bail: + if(prefix) MP4DisposeHandle(prefix); + if(description) MP4DisposeHandle(description); + TEST_RETURN(err); return err; } @@ -327,6 +361,9 @@ ISOGetSampleGroupSampleNumbers(MP4Media media, u32 groupType, u32 groupIndex, return err; } +/* TODO: add an API that will get sample numbers based on T35 header (if it35 is used for marking + * samples) */ + MP4_EXTERN(MP4Err) ISOSetSampleDependency(MP4Media media, s32 sample_index, MP4Handle dependencies) { diff --git a/IsoLib/libisomediafile/src/MP4Movies.c b/IsoLib/libisomediafile/src/MP4Movies.c index 26f00f11..778832e6 100644 --- a/IsoLib/libisomediafile/src/MP4Movies.c +++ b/IsoLib/libisomediafile/src/MP4Movies.c @@ -818,6 +818,33 @@ MP4GetMovieIndTrackSampleEntryType(MP4Movie theMovie, u32 idx, u32 *SEType) return err; } +MP4_EXTERN(MP4Err) +MP4GetMovieIndTrackNALUnitLength(MP4Movie theMovie, u32 idx, u32 *naluLength) +{ + MP4Err err; + MP4Track trak; + MP4TrackReader reader; + MP4Handle sampleEntryH; + + MP4NewHandle(0, &sampleEntryH); + + err = MP4GetMovieIndTrack(theMovie, idx, &trak); + if(err) goto bail; + + err = MP4CreateTrackReader(trak, &reader); + if(err) goto bail; + + err = MP4TrackReaderGetCurrentSampleDescription(reader, sampleEntryH); + if(err) goto bail; + + err = ISOGetNALUnitLength(sampleEntryH, naluLength); + +bail: + TEST_RETURN(err); + MP4DisposeHandle(sampleEntryH); + return err; +} + MP4_EXTERN(MP4Err) MP4GetMovieTrack(MP4Movie theMovie, u32 trackID, MP4Track *outTrack) { diff --git a/IsoLib/libisomediafile/src/MP4Movies.h b/IsoLib/libisomediafile/src/MP4Movies.h index 04383604..c960bbd6 100644 --- a/IsoLib/libisomediafile/src/MP4Movies.h +++ b/IsoLib/libisomediafile/src/MP4Movies.h @@ -53,6 +53,7 @@ extern "C" MP4InvalidMediaErr = -8, /**< Invalid media */ MP4InternalErr = -9, /**< Iternal error */ MP4NotFoundErr = -10, /**< Not found */ + MP4DuplicateErr = -11, /**< Duplicate match */ MP4DataEntryTypeNotSupportedErr = -100, /**< Data entity type not supported */ MP4NoQTAtomErr = -500, /**< No QT atom */ MP4NotImplementedErr = -1000 /**< Not implemented */ @@ -764,6 +765,16 @@ extern "C" */ MP4_EXTERN(MP4Err) MP4GetMovieIndTrackSampleEntryType(MP4Movie theMovie, u32 idx, u32 *SEType); + /** + * @brief Get number of bytes that is used to signal the length of a NAL unit. + * + * @note This function only returns the NALU length of the first sample entry. + * @param theMovie input movie object + * @param idx index of the track ranges between 1 and the number of tracks in theMovie. + * @param naluLength [out] number of bytes to signal NAL unit length. + */ + MP4_EXTERN(MP4Err) MP4GetMovieIndTrackNALUnitLength(MP4Movie theMovie, u32 idx, u32 *naluLength); + /* MP4_EXTERN ( MP4Err ) MP4GetMovieInitialBIFSTrack( MP4Movie theMovie, MP4Track *outBIFSTrack ); @@ -1098,6 +1109,19 @@ extern "C" */ MP4_EXTERN(MP4Err) ISOAddGroupDescription(MP4Media media, u32 groupType, MP4Handle description, u32 *index); + /** + * @brief Adds a T.35 Sample Group Description to the indicated media. + * + * @param media input media object + * @param itu_t_t35_data pre-serialized (big-endian) T.35 data that will go inside sgpd + * @param complete_message_flag If set to 1 indicates that the entire T.35 is stored in + * itu_t_t35_data + * @param index output index of the added group + * @return MP4Err error code + */ + MP4_EXTERN(MP4Err) + ISOAddT35GroupDescription(MP4Media media, MP4Handle itu_t_t35_data, u32 complete_message_flag, + u32 *index); /** * @brief Returns in the handle ‘description’ the group description associated with the given * group index of the given group type. @@ -1736,9 +1760,52 @@ extern "C" */ MP4_EXTERN(MP4Err) MP4CreateTrackReader(MP4Track theTrack, MP4TrackReader *outReader); /** - * @brief Select local_key for reading. Demux mebx track. + * @brief Set local_key_id for reading. Demux mebx track. + */ + MP4_EXTERN(MP4Err) MP4SetMebxTrackReaderLocalKeyId(MP4TrackReader theReader, u32 local_key_id); + /** + * @brief Select the first matching 'mebx' key for a track reader by namespace and value. + * + * Looks up the first key in the 'mebx' sample description matching @p key_namespace and @p + * key_value. If found, sets the corresponding local_key_id on the reader. Optionally returns the + * resolved local_key_id. + * + * If multiple keys match the same namespace and value, this function selects only the first one. + * Use MP4FindMebxKeyMatchByIndex to iterate through all matches. + * + * @param theReader 'mebx' track reader. + * @param key_namespace key namespace from MetadataKeyDeclarationBox + * @param key_value key value from MetadataKeyDeclarationBox + * @param outLocalKeyId Optional; receives local_key_id if non-NULL. + * + * @return MP4NoErr if found and set, MP4NotFoundErr if not found, or error code. + */ + MP4_EXTERN(MP4Err) + MP4SelectFirstMebxTrackReaderKey(MP4TrackReader theReader, u32 key_namespace, MP4Handle key_value, + u32 *outLocalKeyId); + + /** + * @brief Find a specific match of key_namespace + key_value by match index. + * + * This function searches for all entries matching the given key_namespace and key_value, + * and returns information about the match at the specified index (0-based). + * + * Use this to iterate through all matches when multiple entries have the same + * key_namespace and key_value but different setupInfo or other parameters. + * + * @param sampleEntryH Handle to the mebx sample entry + * @param key_namespace Namespace to match + * @param key_value Handle containing the key value to match + * @param matchIndex Zero-based index of which match to return (0=first match, 1=second, etc.) + * @param outAbsoluteIndex Output: absolute index in metadata config array (for use with + * ISOGetMebxMetadataConfig) + * @param outLocalKeyId Output: local_key_id for this match + * @return MP4NoErr if match found, MP4NotFoundErr if matchIndex exceeds available matches */ - MP4_EXTERN(MP4Err) MP4SetMebxTrackReader(MP4TrackReader theReader, u32 local_key); + MP4_EXTERN(MP4Err) + MP4FindMebxKeyMatchByIndex(MP4Handle sampleEntryH, u32 key_namespace, MP4Handle key_value, + u32 matchIndex, u32 *outAbsoluteIndex, u32 *outLocalKeyId); + /** * @brief Frees up resources associated with a track reader. */ diff --git a/IsoLib/libisomediafile/src/MP4OrdinaryTrackReader.c b/IsoLib/libisomediafile/src/MP4OrdinaryTrackReader.c index bfddd008..ce89671e 100644 --- a/IsoLib/libisomediafile/src/MP4OrdinaryTrackReader.c +++ b/IsoLib/libisomediafile/src/MP4OrdinaryTrackReader.c @@ -153,7 +153,7 @@ static MP4Err getNextAccessUnit(struct MP4TrackReaderStruct *self, MP4Handle out if(length == 0) break; type = (unsigned char)data[4] << 24 | (unsigned char)data[5] << 16 | (unsigned char)data[6] << 8 | (unsigned char)data[7]; - if(type == self->mebx_local_key) + if(type == self->mebx_local_key_id) { *outSize = length - 8; err = MP4SetHandleSize(outAccessUnit, *outSize); diff --git a/IsoLib/libisomediafile/src/MP4TrackReader.c b/IsoLib/libisomediafile/src/MP4TrackReader.c index 6128fc6d..a2417c36 100644 --- a/IsoLib/libisomediafile/src/MP4TrackReader.c +++ b/IsoLib/libisomediafile/src/MP4TrackReader.c @@ -252,7 +252,7 @@ MP4TrackReaderGetCurrentSampleNumber(MP4TrackReader theReader, u32 *sampleNumber } MP4_EXTERN(MP4Err) -MP4SetMebxTrackReader(MP4TrackReader theReader, u32 local_key) +MP4SetMebxTrackReaderLocalKeyId(MP4TrackReader theReader, u32 local_key_id) { MP4Err err; MP4TrackReaderPtr reader; @@ -261,7 +261,144 @@ MP4SetMebxTrackReader(MP4TrackReader theReader, u32 local_key) if(theReader == 0) BAILWITHERROR(MP4BadParamErr) reader = (MP4TrackReaderPtr)theReader; - reader->mebx_local_key = local_key; + reader->mebx_local_key_id = local_key_id; + +bail: + TEST_RETURN(err); + return err; +} + +MP4_EXTERN(MP4Err) +MP4SelectFirstMebxTrackReaderKey(MP4TrackReader theReader, u32 key_namespace, MP4Handle key_value, + u32 *outLocalKeyId) +{ + MP4TrackReaderPtr reader; + MP4Handle sampleEntryH = NULL; + MP4Err err = MP4NoErr; + u32 key_cnt = 0; + u32 found_local_id = 0; + int found = 0; + + if((theReader == 0) || (key_value == 0)) BAILWITHERROR(MP4BadParamErr); + reader = (MP4TrackReaderPtr)theReader; + + err = MP4NewHandle(0, &sampleEntryH); + if(err) goto bail; + err = MP4TrackReaderGetCurrentSampleDescription(theReader, sampleEntryH); + if(err) goto bail; + + err = ISOGetMebxMetadataCount(sampleEntryH, &key_cnt); + if(err) goto bail; + + for(u32 i = 0; i < key_cnt; i++) + { + u32 local_id = 0; + u32 ns = 0; + MP4Handle valH = NULL; + + err = MP4NewHandle(0, &valH); + if(err) goto bail; + + err = ISOGetMebxMetadataConfig(sampleEntryH, i, &local_id, &ns, valH, NULL, NULL); + if(err) + { + MP4DisposeHandle(valH); + goto bail; + } + + /* Check if this key and value matches */ + if(ns == key_namespace) + { + u32 inSize = 0, valSize = 0; + MP4GetHandleSize(key_value, &inSize); + MP4GetHandleSize(valH, &valSize); + + if(inSize == valSize && memcmp(*key_value, *valH, inSize) == 0) + { + /* Found first match - save it and stop searching */ + found_local_id = local_id; + found = 1; + MP4DisposeHandle(valH); + break; + } + } + MP4DisposeHandle(valH); + } + + if(!found) + { + err = MP4NotFoundErr; + goto bail; + } + + /* Set internal local_key_id and return if the user wanted */ + reader->mebx_local_key_id = found_local_id; + if(outLocalKeyId) *outLocalKeyId = found_local_id; + + /* Note: This function selects the first match only. + * Use MP4FindMebxKeyMatchByIndex to iterate through all matches. */ + +bail: + if(sampleEntryH) MP4DisposeHandle(sampleEntryH); + TEST_RETURN(err); + return err; +} + +MP4_EXTERN(MP4Err) +MP4FindMebxKeyMatchByIndex(MP4Handle sampleEntryH, u32 key_namespace, MP4Handle key_value, + u32 matchIndex, u32 *outAbsoluteIndex, u32 *outLocalKeyId) +{ + MP4Err err = MP4NoErr; + u32 key_cnt = 0; + u32 found_count = 0; + + if((sampleEntryH == NULL) || (key_value == NULL)) BAILWITHERROR(MP4BadParamErr); + + err = ISOGetMebxMetadataCount(sampleEntryH, &key_cnt); + if(err) goto bail; + + for(u32 i = 0; i < key_cnt; i++) + { + u32 local_id = 0; + u32 ns = 0; + MP4Handle valH = NULL; + + err = MP4NewHandle(0, &valH); + if(err) goto bail; + + err = ISOGetMebxMetadataConfig(sampleEntryH, i, &local_id, &ns, valH, NULL, NULL); + if(err) + { + MP4DisposeHandle(valH); + goto bail; + } + + /* Check if this key and value matches */ + if(ns == key_namespace) + { + u32 inSize = 0, valSize = 0; + MP4GetHandleSize(key_value, &inSize); + MP4GetHandleSize(valH, &valSize); + + if(inSize == valSize && memcmp(*key_value, *valH, inSize) == 0) + { + /* This is a match - check if it's the one we're looking for */ + if(found_count == matchIndex) + { + /* Found the requested match */ + if(outAbsoluteIndex) *outAbsoluteIndex = i; + if(outLocalKeyId) *outLocalKeyId = local_id; + MP4DisposeHandle(valH); + goto bail; + } + found_count++; + } + } + MP4DisposeHandle(valH); + } + + /* If we get here, matchIndex was beyond available matches */ + err = MP4NotFoundErr; bail: TEST_RETURN(err); diff --git a/IsoLib/libisomediafile/src/MP4TrackReader.h b/IsoLib/libisomediafile/src/MP4TrackReader.h index 4b8f780d..8bb77b5d 100644 --- a/IsoLib/libisomediafile/src/MP4TrackReader.h +++ b/IsoLib/libisomediafile/src/MP4TrackReader.h @@ -59,7 +59,7 @@ typedef struct MP4TrackReaderStruct TRACK_READER_ENTRIES u32 isODTrack; u32 isMebxTrack; - u32 mebx_local_key; + u32 mebx_local_key_id; } *MP4TrackReaderPtr; MP4Err MP4CreateMebxTrackReader(MP4Movie theMovie, MP4Track theTrack, MP4TrackReaderPtr *outReader); diff --git a/IsoLib/libisomediafile/src/MetadataKeyTableBox.c b/IsoLib/libisomediafile/src/MetadataKeyTableBox.c index 37628c77..0f6cfa15 100644 --- a/IsoLib/libisomediafile/src/MetadataKeyTableBox.c +++ b/IsoLib/libisomediafile/src/MetadataKeyTableBox.c @@ -87,6 +87,21 @@ static MP4Err serialize(struct MP4Atom *s, char *buffer) if(err) goto bail; buffer += self->bytesWritten; + /* Apple QTFF style: write version/flags and entry_count */ + if(self->isAppleStyle) + { + u32 versionFlags = (self->version << 24) | (self->flags & 0xFFFFFF); + u32 count = 0; + PUT32_V(versionFlags); + if(self->metadataKeyBoxList) + { + err = MP4GetListEntryCount(self->metadataKeyBoxList, &count); + if(err) goto bail; + } + PUT32_V(count); + } + + /* Serialize child boxes/entries */ if(self->metadataKeyBoxList) { u32 count, i; @@ -127,6 +142,12 @@ static MP4Err calculateSize(struct MP4Atom *s) err = MP4CalculateBaseAtomFieldSize(s); if(err) goto bail; + /* Apple QTFF style: add 8 bytes for version/flags + entry_count */ + if(self->isAppleStyle) + { + self->size += 8; + } + if(self->metadataKeyBoxList) { u32 count, i; @@ -182,8 +203,14 @@ static MP4Err createFromInputStream(MP4AtomPtr s, MP4AtomPtr proto, MP4InputStre inputStream->available = available; inputStream->indent = indent; fm->current_offset = currentOffset; + + /* Mark this as Apple-style and read version/flags + entry_count */ + self->isAppleStyle = 1; GET32_V_MSG(temp, "QTFF: version+flags"); + self->version = (temp >> 24) & 0xFF; + self->flags = temp & 0xFFFFFF; GET32_V_MSG(cnt, "QTFF: Entry_count"); + for(i = 0; i < cnt; i++) { MP4Handle key_valH; @@ -197,6 +224,8 @@ static MP4Err createFromInputStream(MP4AtomPtr s, MP4AtomPtr proto, MP4InputStre } else if(err == MP4NoErr) { + /* Standard MEBX format */ + self->isAppleStyle = 0; self->bytesRead += atom->size; err = self->addMetaDataKeyBox(self, atom); if(err) goto bail; diff --git a/IsoLib/libisomediafile/src/SampleGroupDescriptionAtom.c b/IsoLib/libisomediafile/src/SampleGroupDescriptionAtom.c index 9b8ad266..92a57cb0 100644 --- a/IsoLib/libisomediafile/src/SampleGroupDescriptionAtom.c +++ b/IsoLib/libisomediafile/src/SampleGroupDescriptionAtom.c @@ -1,25 +1,16 @@ -/* -This software module was originally developed by Apple Computer, Inc. -in the course of development of MPEG-4. -This software module is an implementation of a part of one or -more MPEG-4 tools as specified by MPEG-4. -ISO/IEC gives users of MPEG-4 free license to this -software module or modifications thereof for use in hardware -or software products claiming conformance to MPEG-4. -Those intending to use this software module in hardware or software -products are advised that its use may infringe existing patents. -The original developer of this software module and his/her company, -the subsequent editors and their companies, and ISO/IEC have no -liability for use of this software module or modifications thereof -in an implementation. -Copyright is not released for non MPEG-4 conforming -products. Apple Computer, Inc. retains full right to use the code for its own -purpose, assign or donate the code to a third party and to -inhibit third parties from using the code for non -MPEG-4 conforming products. -This copyright notice must be included in all copies or -derivative works. Copyright (c) 1999. -*/ +/* This software module was originally developed by Apple Computer, Inc. in the course of + * development of MPEG-4. This software module is an implementation of a part of one or more MPEG-4 + * tools as specified by MPEG-4. ISO/IEC gives users of MPEG-4 free license to this software module + * or modifications thereof for use in hardware or software products claiming conformance to MPEG-4. + * Those intending to use this software module in hardware or software products are advised that its + * use may infringe existing patents. The original developer of this software module and his/her + * company, the subsequent editors and their companies, and ISO/IEC have no liability for use of + * this software module or modifications thereof in an implementation. Copyright is not released for + * non MPEG-4 conforming products. Apple Computer, Inc. retains full right to use the code for its + * own purpose, assign or donate the code to a third party and to inhibit third parties from using + * the code for non MPEG-4 conforming products. This copyright notice must be included in all copies + * or derivative works. Copyright (c) 1999. + */ #include "MP4Atoms.h" #include @@ -49,9 +40,14 @@ static MP4Err addGroupDescription(struct MP4SampleGroupDescriptionAtom *self, sampleGroupEntry *p; u32 theSize, foundIdx; - /* make sure we don't add duplicate descriptions */ + /* Find-or-add: reuse existing description if found */ err = self->findGroupDescriptionIdx(self, theDescription, &foundIdx); - if(err != MP4NotFoundErr) BAILWITHERROR(MP4BadParamErr); + if(err == MP4NoErr) + { + /* Found existing entry - reuse it */ + *index = foundIdx; + return MP4NoErr; + } if(self->groups == NULL) self->groups = calloc(1, sizeof(sampleGroupEntry)); else @@ -72,7 +68,6 @@ static MP4Err addGroupDescription(struct MP4SampleGroupDescriptionAtom *self, bail: TEST_RETURN(err); - return err; } diff --git a/IsoLib/libisomediafile/src/SampleTableAtom.c b/IsoLib/libisomediafile/src/SampleTableAtom.c index 4edb2e7c..e5f42cc0 100644 --- a/IsoLib/libisomediafile/src/SampleTableAtom.c +++ b/IsoLib/libisomediafile/src/SampleTableAtom.c @@ -737,6 +737,10 @@ static MP4Err getSampleGroupSampleNumbers(struct MP4SampleTableAtom *self, u32 g (*outSampleNumbers)[(*outSampleCnt)++] = i; } } + else + { + /* TODO: make sure we can also get it based on default_group_description_index, */ + } bail: TEST_RETURN(err); diff --git a/IsoLib/libisomediafile/src/T35MetadataSampleEntry.c b/IsoLib/libisomediafile/src/T35MetadataSampleEntry.c new file mode 100644 index 00000000..c2b14db2 --- /dev/null +++ b/IsoLib/libisomediafile/src/T35MetadataSampleEntry.c @@ -0,0 +1,213 @@ +/* This software module was originally developed by Apple Computer, Inc. in the course of + * development of MPEG-4. This software module is an implementation of a part of one or more MPEG-4 + * tools as specified by MPEG-4. ISO/IEC gives users of MPEG-4 free license to this software module + * or modifications thereof for use in hardware or software products claiming conformance to MPEG-4. + * Those intending to use this software module in hardware or software products are advised that its + * use may infringe existing patents. The original developer of this software module and his/her + * company, the subsequent editors and their companies, and ISO/IEC have no liability for use of + * this software module or modifications thereof in an implementation. Copyright is not released for + * non MPEG-4 conforming products. Apple Computer, Inc. retains full right to use the code for its + * own purpose, assign or donate the code to a third party and to inhibit third parties from using + * the code for non MPEG-4 conforming products. This copyright notice must be included in all copies + * or derivative works. Copyright (c) 2026. + */ + +#include "MP4Atoms.h" +#include +#include + +static void destroy(MP4AtomPtr s) +{ + MP4T35MetadataSampleEntryPtr self = (MP4T35MetadataSampleEntryPtr)s; + if(self == NULL) return; + + if(self->description) + { + free(self->description); + self->description = NULL; + } + + if(self->t35_identifier) + { + free(self->t35_identifier); + self->t35_identifier = NULL; + } + + if(self->super) self->super->destroy(s); +} + +static MP4Err serialize(struct MP4Atom *s, char *buffer) +{ + MP4Err err; + MP4T35MetadataSampleEntryPtr self = (MP4T35MetadataSampleEntryPtr)s; + + err = MP4SerializeCommonBaseAtomFields(s, buffer); + if(err) goto bail; + buffer += self->bytesWritten; + + PUTBYTES(self->reserved, 6); + PUT16(dataReferenceIndex); + + /* Write description as null-terminated UTF-8 string */ + if(self->description != NULL) + { + u32 descLen = (u32)strlen(self->description) + 1; /* Include null terminator */ + PUTBYTES(self->description, descLen); + } + else + { + /* Empty description: just write '\0' */ + u8 nullByte = 0; + PUT8_V(nullByte); + } + + /* Write t35_identifier byte array */ + if(self->t35_identifier != NULL && self->t35_identifier_size > 0) + { + PUTBYTES(self->t35_identifier, self->t35_identifier_size); + } + + assert(self->bytesWritten == self->size); +bail: + TEST_RETURN(err); + return err; +} + +static MP4Err calculateSize(struct MP4Atom *s) +{ + MP4Err err; + MP4T35MetadataSampleEntryPtr self = (MP4T35MetadataSampleEntryPtr)s; + + err = MP4CalculateBaseAtomFieldSize(s); + if(err) goto bail; + + self->size += (6 + 2); /* reserved + dataReferenceIndex */ + + /* Add description size (null-terminated string) */ + if(self->description != NULL) + { + self->size += (u32)strlen(self->description) + 1; + } + else + { + self->size += 1; /* Just '\0' */ + } + + /* Add t35_identifier size */ + if(self->t35_identifier != NULL) + { + self->size += self->t35_identifier_size; + } + +bail: + TEST_RETURN(err); + return err; +} + +static MP4Err createFromInputStream(MP4AtomPtr s, MP4AtomPtr proto, MP4InputStreamPtr inputStream) +{ + MP4Err err; + MP4T35MetadataSampleEntryPtr self = (MP4T35MetadataSampleEntryPtr)s; + s64 bytesToRead; + u8 *buf = NULL; + u32 i; + + if(self == NULL) BAILWITHERROR(MP4BadParamErr) + err = self->super->createFromInputStream(s, proto, (char *)inputStream); + if(err) goto bail; + + GETBYTES(6, reserved); + GET16(dataReferenceIndex); + + /* Read all remaining bytes into a flat buffer, then split on the first null byte. + * Scanning byte-by-byte and "rewinding" by adjusting only bytesRead (while leaving + * the stream cursor in place) does not work — readData always reads from the current + * stream position. */ + bytesToRead = (s64)(self->size - self->bytesRead); + if(bytesToRead < 0) BAILWITHERROR(MP4BadDataErr); + + if(bytesToRead > 0) + { + buf = (u8 *)calloc((u32)bytesToRead, 1); + TESTMALLOC(buf); + err = inputStream->readData(inputStream, (u32)bytesToRead, (char *)buf, NULL); + if(err) goto bail; + self->bytesRead += (u32)bytesToRead; + + /* Find the null terminator that ends the description field */ + u32 nullPos = (u32)bytesToRead; /* default: no null found */ + for(i = 0; i < (u32)bytesToRead; i++) + { + if(buf[i] == 0) + { + nullPos = i; + break; + } + } + + /* Description: bytes [0 .. nullPos] (including the null terminator) */ + u32 descLen = nullPos + 1; /* length including null */ + self->description = (char *)calloc(descLen, 1); + TESTMALLOC(self->description); + memcpy(self->description, buf, descLen); + + /* t35_identifier: bytes after the null terminator */ + /* TODO: the length of t35_identifier is currently inferred as "all bytes remaining in the + * box after the description null terminator", which prevents any optional boxes (e.g. + * BitRateBox) from following it and makes the format non-extensible. + * This must be resolved at the next MPEG meeting, either by: + * (a) preceding t35_identifier with an explicit length field (e.g. unsigned int(8)), or + * (b) wrapping description and t35_identifier in their own child boxes (e.g. 'hrsd' for + * the human-readable description as already proposed in the amendment text). + * Until then, optional boxes MUST NOT be appended after t35_identifier. */ + u32 identStart = descLen; + if(identStart < (u32)bytesToRead) + { + self->t35_identifier_size = (u32)bytesToRead - identStart; + self->t35_identifier = (u8 *)calloc(self->t35_identifier_size, 1); + TESTMALLOC(self->t35_identifier); + memcpy(self->t35_identifier, buf + identStart, self->t35_identifier_size); + } + + free(buf); + buf = NULL; + } + + if(self->bytesRead != self->size) BAILWITHERROR(MP4BadDataErr) + +bail: + free(buf); + TEST_RETURN(err); + return err; +} + +MP4Err MP4CreateT35MetadataSampleEntry(MP4T35MetadataSampleEntryPtr *outAtom) +{ + MP4Err err; + MP4T35MetadataSampleEntryPtr self; + + self = (MP4T35MetadataSampleEntryPtr)calloc(1, sizeof(MP4T35MetadataSampleEntry)); + TESTMALLOC(self) + + err = MP4CreateBaseAtom((MP4AtomPtr)self); + if(err) goto bail; + + self->type = MP4T35MetadataSampleEntryType; + self->name = "T35MetadataSampleEntry"; + self->createFromInputStream = (cisfunc)createFromInputStream; + self->destroy = destroy; + self->calculateSize = calculateSize; + self->serialize = serialize; + + self->dataReferenceIndex = 1; + memset(self->reserved, 0, 6); + + self->description = NULL; + self->t35_identifier = NULL; + self->t35_identifier_size = 0; + + *outAtom = self; +bail: + TEST_RETURN(err); + return err; +} diff --git a/IsoLib/libisomediafile/src/TimeToSampleAtom.c b/IsoLib/libisomediafile/src/TimeToSampleAtom.c index 2d68ac47..a10a9c48 100644 --- a/IsoLib/libisomediafile/src/TimeToSampleAtom.c +++ b/IsoLib/libisomediafile/src/TimeToSampleAtom.c @@ -150,6 +150,13 @@ static MP4Err extendLastSampleDuration(struct MP4TimeToSampleAtom *self, u32 dur err = MP4NoErr; current = (sttsEntryPtr)self->currentEntry; + if(current == NULL) + { + /* lets treat it as the first sample */ + err = addSample(self, duration); + goto bail; + } + if(current->sampleCount == 1) { current->sampleDuration += duration; @@ -163,7 +170,6 @@ static MP4Err extendLastSampleDuration(struct MP4TimeToSampleAtom *self, u32 dur bail: TEST_RETURN(err); - return err; } diff --git a/IsoLib/libisomediafile/src/TrackAtom.c b/IsoLib/libisomediafile/src/TrackAtom.c index bf40cf32..ebc791a7 100644 --- a/IsoLib/libisomediafile/src/TrackAtom.c +++ b/IsoLib/libisomediafile/src/TrackAtom.c @@ -1,25 +1,16 @@ -/* -This software module was originally developed by Apple Computer, Inc. -in the course of development of MPEG-4. -This software module is an implementation of a part of one or -more MPEG-4 tools as specified by MPEG-4. -ISO/IEC gives users of MPEG-4 free license to this -software module or modifications thereof for use in hardware -or software products claiming conformance to MPEG-4. -Those intending to use this software module in hardware or software -products are advised that its use may infringe existing patents. -The original developer of this software module and his/her company, -the subsequent editors and their companies, and ISO/IEC have no -liability for use of this software module or modifications thereof -in an implementation. -Copyright is not released for non MPEG-4 conforming -products. Apple Computer, Inc. retains full right to use the code for its own -purpose, assign or donate the code to a third party and to -inhibit third parties from using the code for non -MPEG-4 conforming products. -This copyright notice must be included in all copies or -derivative works. Copyright (c) 1999. -*/ +/* This software module was originally developed by Apple Computer, Inc. in the course of + * development of MPEG-4. This software module is an implementation of a part of one or more MPEG-4 + * tools as specified by MPEG-4. ISO/IEC gives users of MPEG-4 free license to this software module + * or modifications thereof for use in hardware or software products claiming conformance to MPEG-4. + * Those intending to use this software module in hardware or software products are advised that its + * use may infringe existing patents. The original developer of this software module and his/her + * company, the subsequent editors and their companies, and ISO/IEC have no liability for use of + * this software module or modifications thereof in an implementation. Copyright is not released for + * non MPEG-4 conforming products. Apple Computer, Inc. retains full right to use the code for its + * own purpose, assign or donate the code to a third party and to inhibit third parties from using + * the code for non MPEG-4 conforming products. This copyright notice must be included in all copies + * or derivative works. Copyright (c) 1999. + */ /* $Id: TrackAtom.c,v 1.1.1.1 2002/09/20 08:53:35 julien Exp $ */ diff --git a/IsoLib/t35_tool/CMakeLists.txt b/IsoLib/t35_tool/CMakeLists.txt new file mode 100644 index 00000000..7d6fd856 --- /dev/null +++ b/IsoLib/t35_tool/CMakeLists.txt @@ -0,0 +1,94 @@ +cmake_minimum_required(VERSION 3.16) + +# Name & languages +project(t35_tool LANGUAGES C CXX) + +# C++ standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +# Includes: libisomediafile core +include_directories( + ../libisomediafile/src +) + +# Platform-specific include roots for libisomediafile +include_directories( + # Linux + $<$:${CMAKE_CURRENT_LIST_DIR}/../libisomediafile/linux> + # Windows + $<$:${CMAKE_CURRENT_LIST_DIR}/../libisomediafile/w32> + # macOS + $<$:${CMAKE_CURRENT_LIST_DIR}/../libisomediafile/macosx> +) + +# common warnings +if (MSVC) + add_compile_options(/W4) + add_definitions(-D_CRT_SECURE_NO_WARNINGS) +else() + add_compile_options(-Wall -Wextra -Wpedantic) +endif() + +# ---- Dependencies ---- +include(FetchContent) + +# JSON dependency +FetchContent_Declare( + json + GIT_REPOSITORY https://github.com/nlohmann/json.git + GIT_TAG v3.12.0 +) +FetchContent_MakeAvailable(json) + +# CLI11 (header-only) +FetchContent_Declare( + cli11 + GIT_REPOSITORY https://github.com/CLIUtils/CLI11 + GIT_TAG v2.5.0 +) +FetchContent_MakeAvailable(cli11) + +# spdlog (logging library) +FetchContent_Declare( + spdlog + GIT_REPOSITORY https://github.com/gabime/spdlog.git + GIT_TAG v1.16.0 +) +FetchContent_MakeAvailable(spdlog) + +# ---- t35_tool ---- +add_executable( + t35_tool + t35_tool.cpp + sources/SMPTE_ST2094_50.cpp + common/MetadataTypes.cpp + common/T35Prefix.cpp + common/Logger.cpp + sources/MetadataSource.cpp + sources/GenericJsonSource.cpp + sources/SMPTEFolderSource.cpp + injection/InjectionStrategy.cpp + injection/MebxMe4cStrategy.cpp + injection/DedicatedIt35Strategy.cpp + injection/SampleGroupStrategy.cpp + extraction/ExtractionStrategy.cpp + extraction/MebxMe4cExtractor.cpp + extraction/DedicatedIt35Extractor.cpp + extraction/SampleGroupExtractor.cpp + extraction/AutoExtractor.cpp + extraction/SeiExtractor.cpp +) + +target_link_libraries( + t35_tool + PRIVATE + libisomediafile + nlohmann_json::nlohmann_json + CLI11::CLI11 + spdlog::spdlog +) + + + diff --git a/IsoLib/t35_tool/README.md b/IsoLib/t35_tool/README.md new file mode 100644 index 00000000..9a3907f2 --- /dev/null +++ b/IsoLib/t35_tool/README.md @@ -0,0 +1,411 @@ +# T.35 Metadata Tool v2.0 + +A modular command-line tool for injecting and extracting ITU-T T.35 metadata into/from MP4 video files. This tool is specifically designed for handling SMPTE ST 2094-50 dynamic metadata and other T.35-based metadata formats. + +## Overview + +The T.35 Metadata Tool provides a clean, modular architecture for working with T.35 metadata in MP4 containers. It supports multiple injection and extraction methods, allowing flexibility in how metadata is embedded and retrieved from video files. + +### Key Features + +- **Multiple Injection Methods**: Support for MEBX tracks (me4c namespace), dedicated metadata tracks, and sample groups +- **Flexible Extraction**: Auto-detection or manual selection of extraction strategies +- **Multiple Source Formats**: JSON manifests with binary references, SMPTE folder structures +- **T.35 Prefix Support**: Configurable ITU-T T.35 country/terminal provider codes +- **Validation**: Built-in validation for metadata integrity and applicability +- **Clean Architecture**: Separation of concerns with pluggable sources and strategies + +## Architecture + +The tool follows a modular design with three main components: + +1. **Sources** - Handle input metadata formats + - JSON manifest with binary file references + - SMPTE folder structures with JSON files + +2. **Injection Strategies** - Define how metadata is embedded into MP4 + - MEBX track with me4c namespace + - Dedicated metadata track (it35) + - Sample groups + +3. **Extraction Strategies** - Define how metadata is retrieved from MP4 + - Auto-detection (tries all methods) + - MEBX extraction (me4c) + - Dedicated track extraction + - Sample group extraction + - SEI conversion (stub) + +## Building + +### Prerequisites + +- C++17 compatible compiler +- CMake 3.15 or later +- CLI11 library (command-line parsing) +- libisomediafile (MP4 manipulation) + +### Build Instructions + +```bash +mkdir build +cd build +cmake .. +make +``` + +The compiled binary will be available as `t35_tool`. + +## Usage + +### Basic Command Structure + +```bash +t35_tool [OPTIONS] SUBCOMMAND +``` + +### Global Options + +- `--verbose ` - Set verbosity level (0-3) + - 0: Errors only + - 1: Warnings + - 2: Info (default) + - 3: Debug +- `--list-options` - Display all available source types and injection/extraction methods +- `--version, -v` - Show version information + +### Inject Command + +Inject T.35 metadata into an MP4 file. + +```bash +t35_tool inject [OPTIONS] input output +``` + +#### Required Arguments + +- `input` - Input MP4 file path +- `output` - Output MP4 file path + +#### Options + +- `--source, -s ` - Source specification in format `type:path` (required) + - `json-manifest:/path/to/manifest.json` + - `smpte-folder:/path/to/folder` + - `generic-json:/path/to/file.json` (alias for json-manifest) + - `json-folder:/path/to/folder` (alias for smpte-folder) + +- `--method, -m ` - Injection method (default: `mebx-me4c`) + - `mebx-me4c` - MEBX track with me4c namespace + - `dedicated-it35` - Dedicated metadata track + - `sample-group` - Sample group approach + +- `--t35-prefix, -p ` - T.35 prefix in format `HEX[:description]` + - Default: `B500900001:SMPTE-ST2094-50` + - Format: Country code + terminal provider code (hex) + optional description + +#### Examples + +```bash +# Inject using JSON manifest with default MEBX-me4c method +t35_tool inject input.mp4 output.mp4 \ + --source json-manifest:metadata.json + +# Inject using SMPTE folder with dedicated track method +t35_tool inject input.mp4 output.mp4 \ + --source smpte-folder:./metadata_folder \ + --method dedicated-it35 + +# Inject using sample groups +t35_tool inject input.mp4 output.mp4 \ + --source json-manifest:metadata.json \ + --method sample-group +``` + +### Extract Command + +Extract T.35 metadata from an MP4 file. + +```bash +t35_tool extract [OPTIONS] input output +``` + +#### Required Arguments + +- `input` - Input MP4 file path +- `output` - Output directory or file path + +#### Options + +- `--method, -m ` - Extraction method (default: `auto`) + - `auto` - Auto-detect extraction method + - `mebx-it35` - Extract from MEBX track (it35 namespace) + - `mebx-me4c` - Extract from MEBX track (me4c namespace) + - `dedicated-it35` - Extract from dedicated metadata track + - `sample-group` - Extract from sample groups + - `sei` - Convert to SEI messages (stub) + +- `--t35-prefix, -p ` - T.35 prefix filter + - Default: `B500900001:SMPTE-ST2094-50` + +#### Examples + +```bash +# Auto-detect and extract to directory +t35_tool extract input.mp4 ./output_folder + +# Extract from MEBX-me4c track +t35_tool extract input.mp4 ./output_folder \ + --method mebx-me4c + +# Extract with specific T.35 prefix filter +t35_tool extract input.mp4 ./output_folder \ + --method auto \ + --t35-prefix B500900001:SMPTE-ST2094-50 + +# Extract from sample groups +t35_tool extract input.mp4 ./output_folder \ + --method sample-group +``` + +## Source Formats + +### JSON Manifest (json-manifest / generic-json) + +A simple JSON file that references binary metadata files: + +```json +{ + "metadata": [ + { + "sample": 0, + "file": "metadata_000.bin" + }, + { + "sample": 1, + "file": "metadata_001.bin" + } + ] +} +``` + +Binary files should contain raw T.35 payload data (without the T.35 prefix). + +### SMPTE Folder (smpte-folder / json-folder) + +A directory containing individual JSON files following SMPTE ST 2094-50 format: + +``` +metadata_folder/ +├── frame_0000.json +├── frame_0001.json +├── frame_0002.json +└── ... +``` + +Each JSON file contains SMPTE ST 2094-50 compliant metadata for a single frame. + +## T.35 Prefix Format + +The T.35 prefix identifies the metadata type using ITU-T T.35 country and terminal provider codes: + +- Format: `CCTTTTTTTT[:Description]` + - `CC` - Country code (2 hex digits) + - `TTTTTTTT` - Terminal provider code (8 hex digits) + - `Description` - Optional human-readable description + +### Common Prefixes + +- `B500900001:SMPTE-ST2094-50` - SMPTE ST 2094-50 dynamic metadata +- `B5003C0001:HDR10Plus` - HDR10+ metadata (example) + +## Injection Methods Explained + +### MEBX with me4c Namespace + +- **Identifier**: `mebx-me4c` +- **Description**: Uses the Metadata Extension Box (MEBX) with the 'me4c' namespace +- **Use Case**: Standard metadata embedding for compatibility with various players +- **Container**: MEBX track in MP4 + +### Dedicated Metadata Track + +- **Identifier**: `dedicated-it35` +- **Description**: Creates a separate metadata track specifically for T.35 data +- **Use Case**: When metadata needs to be clearly separated from video streams +- **Container**: Dedicated track with it35 handler + +### Sample Groups + +- **Identifier**: `sample-group` +- **Description**: Associates metadata with video samples using sample-to-group mapping +- **Use Case**: Frame-accurate metadata association with minimal overhead +- **Container**: Sample group boxes in MP4 + +## Extraction Methods + +### Auto Detection + +- **Identifier**: `auto` +- **Description**: Automatically tries all extraction methods and uses the first successful one +- **Recommended**: Yes, for general use + +### Manual Extraction + +Specify the exact extraction method when you know the injection method used: + +- `mebx-me4c` - For MEBX tracks with me4c namespace +- `dedicated-it35` - For dedicated metadata tracks +- `sample-group` - For sample group based metadata + +## Error Handling + +The tool provides detailed error messages for common issues: + +- **Invalid T.35 prefix**: Malformed prefix string +- **Source validation failed**: Invalid source format or missing files +- **Metadata validation failed**: Corrupt or invalid metadata content +- **Strategy not applicable**: Chosen method incompatible with input +- **Injection/Extraction failed**: MP4 manipulation errors + +Exit codes: +- `0` - Success +- `1` - Error occurred (check log output) + +## Logging and Debugging + +Control verbosity with the `--verbose` flag: + +```bash +# Error messages only +t35_tool --verbose 0 inject input.mp4 output.mp4 --source json-manifest:meta.json + +# Full debug output +t35_tool --verbose 3 inject input.mp4 output.mp4 --source json-manifest:meta.json +``` + +Log messages include: +- Input/output file paths +- Source type and validation status +- Injection/extraction method used +- T.35 prefix details +- Success/failure status with error details + +## Workflow Examples + +### Complete Round-Trip Workflow + +1. **Prepare metadata**: + ```json + { + "metadata": [ + {"sample": 0, "file": "frame_000.bin"}, + {"sample": 1, "file": "frame_001.bin"} + ] + } + ``` + +2. **Inject into video**: + ```bash + t35_tool inject original.mp4 with_metadata.mp4 \ + --source json-manifest:manifest.json \ + --method mebx-me4c + ``` + +3. **Verify by extracting**: + ```bash + t35_tool extract with_metadata.mp4 ./extracted \ + --method auto + ``` + +4. **Compare original and extracted metadata**: + ```bash + diff -r original_metadata/ extracted/ + ``` + +### Working with SMPTE Folders + +1. **Organize SMPTE JSON files**: + ``` + smpte_metadata/ + ├── frame_0000.json + ├── frame_0001.json + └── ... + ``` + +2. **Inject**: + ```bash + t35_tool inject video.mp4 video_with_smpte.mp4 \ + --source smpte-folder:./smpte_metadata \ + --method dedicated-it35 \ + --t35-prefix B500900001:SMPTE-ST2094-50 + ``` + +3. **Extract and verify**: + ```bash + t35_tool extract video_with_smpte.mp4 ./extracted_smpte + ``` + +## Limitations and Known Issues + +- SEI conversion (`--method sei`) is currently a stub implementation +- Sample group extraction requires exact frame alignment +- Large metadata files may impact MP4 file size significantly +- Some extraction methods may not preserve exact binary formatting + +## Development + +### Project Structure + +``` +t35_tool/ +├── t35_tool.cpp # Main entry point +├── common/ +│ ├── Logger.hpp # Logging utilities +│ ├── MetadataTypes.hpp # Core data types +│ └── T35Prefix.hpp # T.35 prefix handling +├── sources/ +│ └── MetadataSource.hpp # Source abstraction and factory +├── injection/ +│ └── InjectionStrategy.hpp # Injection strategy abstraction +└── extraction/ + └── ExtractionStrategy.hpp # Extraction strategy abstraction +``` + +### Adding New Sources + +Implement the `MetadataSource` interface and register in the factory. + +### Adding New Injection Methods + +Implement the `InjectionStrategy` interface and register in the factory. + +### Adding New Extraction Methods + +Implement the `ExtractionStrategy` interface and register in the factory. + +## Contributing + +When contributing, please ensure: +- Code follows existing style conventions +- New features include appropriate validation +- Error messages are clear and actionable +- Documentation is updated accordingly + +## License + +[Specify your license here] + +## References + +- ITU-T Recommendation T.35 - Procedure for allocation of ITU-T defined codes +- SMPTE ST 2094-50 - Dynamic Metadata for Color Volume Transform +- ISO/IEC 14496-12 - ISO Base Media File Format + +## Support + +For issues, questions, or contributions, please [specify contact method or issue tracker]. + +--- + +**Version**: 2.0 +**Last Updated**: January 15, 2026 diff --git a/IsoLib/t35_tool/av1_tool.cpp b/IsoLib/t35_tool/av1_tool.cpp new file mode 100644 index 00000000..b0de8e58 --- /dev/null +++ b/IsoLib/t35_tool/av1_tool.cpp @@ -0,0 +1,323 @@ +/** + * @file av1_tool.cpp + * @brief Implementation of a simple AV1 tool for debugging and analysis. + * @version 0.1 + * @date 2025-08-26 + * + * @copyright This software module was originally developed by Apple Computer, Inc. in the course of + * development of MPEG-4. This software module is an implementation of a part of one or more MPEG-4 + * tools as specified by MPEG-4. ISO/IEC gives users of MPEG-4 free license to this software module + * or modifications thereof for use in hardware or software products claiming conformance to MPEG-4 + * only for evaluation and testing purposes. Those intending to use this software module in hardware + * or software products are advised that its use may infringe existing patents. The original + * developer of this software module and his/her company, the subsequent editors and their + * companies, and ISO/IEC have no liability for use of this software module or modifications thereof + * in an implementation. + * + * Copyright is not released for non MPEG-4 conforming products. Apple Computer, Inc. retains full + * right to use the code for its own purpose, assign or donate the code to a third party and to + * inhibit third parties from using the code for non MPEG-4 conforming products. This copyright + * notice must be included in all copies or derivative works. + * + */ + +// libisomediafile headers +extern "C" +{ +#include "MP4Movies.h" +#include "MP4Atoms.h" +} + +// C++ headers +#include +#include +#include +#include + +static void fourccToStr(u32 fcc, char out[5]) +{ + out[0] = char((fcc >> 24) & 0xFF); + out[1] = char((fcc >> 16) & 0xFF); + out[2] = char((fcc >> 8) & 0xFF); + out[3] = char((fcc) & 0xFF); + out[4] = 0; +} + +static std::string sampleFlagsToStr(u32 flags) +{ + std::ostringstream oss; + oss << "0x" << std::hex << flags << std::dec; + + std::vector labels; + if(flags & MP4MediaSampleNotSync) labels.push_back("non-sync"); + else + labels.push_back("sync"); + + if(flags & MP4MediaSampleHasCTSOffset) labels.push_back("CTS-offset"); + + if(!labels.empty()) + { + oss << " ["; + for(size_t i = 0; i < labels.size(); ++i) + { + if(i) oss << ","; + oss << labels[i]; + } + oss << "]"; + } + return oss.str(); +} + +static const char *obuTypeName(unsigned t) +{ + switch(t) + { + case 0: + return "RESERVED_0"; + case 1: + return "SEQUENCE_HEADER"; + case 2: + return "TEMPORAL_DELIMITER"; + case 3: + return "FRAME_HEADER"; + case 4: + return "TILE_GROUP"; + case 5: + return "METADATA"; + case 6: + return "FRAME"; + case 7: + return "REDUNDANT_FRAME_HEADER"; + case 8: + return "TILE_LIST"; + case 9: + return "PADDING"; + default: + return "RESERVED"; + } +} + +struct OBUHeader +{ + unsigned forbidden_bit = 0; + unsigned obu_type = 0; + bool extension_flag = false; + bool has_size_field = false; + unsigned reserved_1bit = 0; + unsigned temporal_id = 0; + unsigned spatial_id = 0; + uint64_t payload_size = 0; + size_t header_bytes = 0; // bytes consumed by the header (incl. ext + leb) +}; + +// Minimal unsigned LEB128 (8-byte cap, as per AV1 limit for size fields) +static bool readULEB128(const uint8_t *p, size_t avail, uint64_t &value, size_t &used) +{ + value = 0; + used = 0; + const int MAX_BYTES = 8; + while(used < avail && used < (size_t)MAX_BYTES) + { + uint8_t byte = p[used]; + if(value >> 57) return false; // would overflow 64-bit with next 7 bits + value |= uint64_t(byte & 0x7F) << (7 * used); + used++; + if((byte & 0x80) == 0) return true; + } + return false; // ran out or exceeded cap without terminator +} + +static bool parseOBUHeader(const uint8_t *data, size_t avail, OBUHeader &h) +{ + if(avail < 1) return false; + uint8_t b0 = data[0]; + h.forbidden_bit = (b0 >> 7) & 0x1; + h.obu_type = (b0 >> 3) & 0x0F; + h.extension_flag = ((b0 >> 2) & 0x1) != 0; + h.has_size_field = ((b0 >> 1) & 0x1) != 0; + h.reserved_1bit = b0 & 0x1; + h.header_bytes = 1; + + if(h.forbidden_bit != 0) return false; // spec requires 0 + + if(h.extension_flag) + { + if(avail < h.header_bytes + 1) return false; + uint8_t ext = data[h.header_bytes]; + h.temporal_id = (ext >> 5) & 0x7; + h.spatial_id = (ext >> 3) & 0x3; + // lower 3 bits are reserved 0 in spec; we won't strictly enforce + h.header_bytes += 1; + } + + if(!h.has_size_field) + { + // In AV1 ISOBMFF low overhead bitstream format is a must. + return false; + } + + if(h.header_bytes >= avail) return false; + size_t leb_used = 0; + uint64_t payload = 0; + if(!readULEB128(data + h.header_bytes, avail - h.header_bytes, payload, leb_used)) return false; + h.payload_size = payload; + h.header_bytes += leb_used; + // Do not check payload bounds here; caller will ensure total fits in sample + return true; +} + +static void dumpAv1SampleOBUs(const uint8_t *sample, size_t sampleSize, uint32_t sampleIndex, + u32 flags, s32 cts, s32 dts) +{ + std::cout << " Sample#" << sampleIndex + 1 << " size=" << sampleSize + << " flags=" << sampleFlagsToStr(flags) << " CTS=" << cts << " DTS=" << dts << "\n"; + + size_t off = 0; + unsigned idx = 0; + while(off < sampleSize) + { + OBUHeader h; + if(!parseOBUHeader(sample + off, sampleSize - off, h)) + { + std::cout << " !! OBU parse error at +" << off << "\n"; + break; + } + size_t total = h.header_bytes + size_t(h.payload_size); + if(off + total > sampleSize) + { + std::cout << " !! Truncated OBU at +" << off << " need=" << total + << " have=" << (sampleSize - off) << "\n"; + break; + } + std::cout << " OBU[" << idx++ << "] @+" << off << " type=" << h.obu_type << " (" + << obuTypeName(h.obu_type) << ")" + << " spatial_id=" << h.spatial_id << " temporal_id=" << h.temporal_id + << " hdr=" << h.header_bytes << " payload=" << h.payload_size + << (h.extension_flag ? " ext tid=" : "") + << (h.extension_flag ? std::to_string(h.temporal_id) : "") + << (h.extension_flag ? " sid=" : "") + << (h.extension_flag ? std::to_string(h.spatial_id) : "") << "\n"; + off += total; + } + if(off != sampleSize) + { + std::cout << " note: leftover=" << (sampleSize - off) << " bytes\n"; + } +} + +int main(int argc, char **argv) +{ + if(argc < 2) + { + std::cerr << "Usage: av1_tool \n"; + return 1; + } + + MP4Err err = MP4NoErr; + MP4Movie moov = nullptr; + + // Open MP4 + err = MP4OpenMovieFile(&moov, argv[1], MP4OpenMovieNormal); + if(err) + { + std::cerr << "Failed to open " << argv[1] << " (err=" << err << ")\n"; + return err; + } + + u32 trackCount = 0; + if((err = MP4GetMovieTrackCount(moov, &trackCount))) + { + MP4DisposeMovie(moov); + return err; + } + std::cout << "Movie has " << trackCount << " tracks\n"; + + for(u32 trackNumber = 1; trackNumber <= trackCount; ++trackNumber) + { + MP4Track trak = nullptr; + if(MP4GetMovieIndTrack(moov, trackNumber, &trak) != MP4NoErr || !trak) continue; + + MP4TrackReader reader = nullptr; + if(MP4CreateTrackReader(trak, &reader) != MP4NoErr || !reader) continue; + + MP4Handle sampleEntryH = nullptr; + MP4NewHandle(0, &sampleEntryH); + if(!sampleEntryH) + { + MP4DisposeTrackReader(reader); + continue; + } + + err = MP4TrackReaderGetCurrentSampleDescription(reader, sampleEntryH); + if(err) + { + MP4DisposeHandle(sampleEntryH); + MP4DisposeTrackReader(reader); + continue; + } + + u32 sampleEntryType = 0; + ISOGetSampleDescriptionType(sampleEntryH, &sampleEntryType); + char typeStr[5]; + fourccToStr(sampleEntryType, typeStr); + std::cout << "Track " << trackNumber << " sample entry: " << typeStr; + + // If resv or encv, get the original format + if(sampleEntryType == MP4RestrictedVideoSampleEntryAtomType || + sampleEntryType == MP4EncVisualSampleEntryAtomType) + { + ISOGetOriginalFormat(sampleEntryH, &sampleEntryType); + fourccToStr(sampleEntryType, typeStr); + std::cout << ":" << typeStr << "\n"; + } + else + { + std::cout << "\n"; + } + + if(sampleEntryType == MP4_FOUR_CHAR_CODE('a', 'v', '0', '1')) + { + std::cout << " AV1 track detected — dumping OBU headers per sample \n"; + + MP4Handle auH = nullptr; + MP4NewHandle(0, &auH); + if(!auH) + { + MP4DisposeHandle(sampleEntryH); + MP4DisposeTrackReader(reader); + continue; + } + + u32 auSize = 0; + u32 flags = 0; + s32 cts = 0, dts = 0; + u32 auIndex = 0; + + while((err = MP4TrackReaderGetNextAccessUnit(reader, auH, &auSize, &flags, &cts, &dts)) == + MP4NoErr) + { + const uint8_t *bytes = (const uint8_t *)*auH; + dumpAv1SampleOBUs(bytes, auSize, auIndex++, flags, cts, dts); + // Clear for next AU (keeps capacity per lib's handle semantics) + MP4SetHandleSize(auH, 0); + } + + if(err != MP4EOF && err != MP4NoErr) + { + std::cerr << " reader error on track " << trackNumber << " (err=" << err << ")\n"; + } + + MP4DisposeHandle(auH); + } + else + { + std::cout << " (not an AV1 track, skipping sample dump)\n"; + } + + MP4DisposeHandle(sampleEntryH); + MP4DisposeTrackReader(reader); + } + + MP4DisposeMovie(moov); + return err; +} diff --git a/IsoLib/t35_tool/common/Logger.cpp b/IsoLib/t35_tool/common/Logger.cpp new file mode 100644 index 00000000..c5a54aff --- /dev/null +++ b/IsoLib/t35_tool/common/Logger.cpp @@ -0,0 +1,51 @@ +#include "Logger.hpp" + +namespace t35 +{ + +std::shared_ptr Logger::logger = nullptr; + +void Logger::init(int verboseLevel) +{ + if(logger) + { + return; // Already initialized + } + + // Create console logger with color + logger = spdlog::stdout_color_mt("t35_tool"); + + // Set level based on verbosity + switch(verboseLevel) + { + case 0: + logger->set_level(spdlog::level::err); + break; + case 1: + logger->set_level(spdlog::level::warn); + break; + case 2: + logger->set_level(spdlog::level::info); + break; + case 3: + default: + logger->set_level(spdlog::level::debug); + break; + } + + // Set pattern: [LEVEL] message + logger->set_pattern("[%^%l%$] %v"); + + LOG_DEBUG("Logger initialized with verbose level {}", verboseLevel); +} + +std::shared_ptr Logger::get() +{ + if(!logger) + { + init(); // Auto-initialize with default level if not already done + } + return logger; +} + +} // namespace t35 diff --git a/IsoLib/t35_tool/common/Logger.hpp b/IsoLib/t35_tool/common/Logger.hpp new file mode 100644 index 00000000..5f6aa31e --- /dev/null +++ b/IsoLib/t35_tool/common/Logger.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include + +namespace t35 { + +/** + * Logger wrapper for t35_tool using spdlog + * Provides simple logging interface with various levels + */ +class Logger { +public: + /** + * Initialize logger (call once at start of program) + * @param verboseLevel 0=error only, 1=warn+error, 2=info+warn+error, 3=debug+all + */ + static void init(int verboseLevel = 2); + + /** + * Get the logger instance + */ + static std::shared_ptr get(); + + // Convenience logging methods + template + static void debug(Args&&... args) { + get()->debug(std::forward(args)...); + } + + template + static void info(Args&&... args) { + get()->info(std::forward(args)...); + } + + template + static void warn(Args&&... args) { + get()->warn(std::forward(args)...); + } + + template + static void error(Args&&... args) { + get()->error(std::forward(args)...); + } + + template + static void critical(Args&&... args) { + get()->critical(std::forward(args)...); + } + +private: + static std::shared_ptr logger; +}; + +} // namespace t35 + +// Convenience macros +#define LOG_DEBUG(...) t35::Logger::debug(__VA_ARGS__) +#define LOG_INFO(...) t35::Logger::info(__VA_ARGS__) +#define LOG_WARN(...) t35::Logger::warn(__VA_ARGS__) +#define LOG_ERROR(...) t35::Logger::error(__VA_ARGS__) +#define LOG_CRITICAL(...) t35::Logger::critical(__VA_ARGS__) diff --git a/IsoLib/t35_tool/common/MetadataTypes.cpp b/IsoLib/t35_tool/common/MetadataTypes.cpp new file mode 100644 index 00000000..42d0b6a0 --- /dev/null +++ b/IsoLib/t35_tool/common/MetadataTypes.cpp @@ -0,0 +1,102 @@ +#include "MetadataTypes.hpp" + +namespace t35 +{ + +bool validateMetadataMap(const MetadataMap &items, std::string &errorMsg) +{ + if(items.empty()) + { + errorMsg = "Metadata map is empty"; + return false; + } + + // Check each item individually + for(const auto &[frameStart, item] : items) + { + // Check frame_start matches map key + if(item.frame_start != frameStart) + { + errorMsg = "Item frame_start (" + std::to_string(item.frame_start) + + ") doesn't match map key (" + std::to_string(frameStart) + ")"; + return false; + } + + // Check duration is positive + if(item.frame_duration == 0) + { + errorMsg = "Item at frame " + std::to_string(frameStart) + " has zero duration"; + return false; + } + + // Check payload is not empty + if(item.payload.empty()) + { + errorMsg = "Item at frame " + std::to_string(frameStart) + " has empty payload"; + return false; + } + } + + // Check for overlaps + MetadataItem prevItem; + bool first = true; + + for(const auto &[frameStart, item] : items) + { + if(!first) + { + if(prevItem.overlaps(item)) + { + errorMsg = "Overlapping metadata entries: frames [" + std::to_string(prevItem.frame_start) + + "-" + std::to_string(prevItem.frameEnd()) + ") and [" + + std::to_string(item.frame_start) + "-" + std::to_string(item.frameEnd()) + ")"; + return false; + } + } + prevItem = item; + first = false; + } + + return true; +} + +bool isFullyCovering(const MetadataMap &items, uint32_t totalFrames) +{ + if(items.empty() || totalFrames == 0) + { + return false; + } + + // Check if first item starts at frame 0 + if(items.begin()->first != 0) + { + return false; + } + + // Check coverage + uint32_t expectedNext = 0; + for(const auto &[frameStart, item] : items) + { + if(frameStart != expectedNext) + { + return false; // Gap detected + } + expectedNext = item.frameEnd(); + } + + // Check if we covered all frames + return expectedNext >= totalFrames; +} + +bool isStaticMetadata(const MetadataMap &items, uint32_t totalFrames) +{ + if(items.size() != 1) + { + return false; + } + + const auto &item = items.begin()->second; + return item.frame_start == 0 && item.frameEnd() >= totalFrames; +} + +} // namespace t35 diff --git a/IsoLib/t35_tool/common/MetadataTypes.hpp b/IsoLib/t35_tool/common/MetadataTypes.hpp new file mode 100644 index 00000000..1b91d9ad --- /dev/null +++ b/IsoLib/t35_tool/common/MetadataTypes.hpp @@ -0,0 +1,173 @@ +#pragma once + +#include +#include +#include +#include +#include + +// Forward declarations from libisomediafile +// These will be properly defined when MP4Movies.h is included +#ifndef MP4Movie +struct MP4MovieRecord; +typedef struct MP4MovieRecord* MP4Movie; +#endif + +#ifndef MP4Track +struct MP4TrackRecord; +typedef struct MP4TrackRecord* MP4Track; +#endif + +namespace t35 { + +// ============================================================================ +// Core Data Types +// ============================================================================ + +/** + * Single metadata item with timing and binary payload + */ +struct MetadataItem { + uint32_t frame_start; // Starting frame number + uint32_t frame_duration; // Duration in frames + std::vector payload; // Binary T.35 payload (without T.35 prefix) + std::string source_info; // Debug: where this came from (filename, etc.) + + MetadataItem() : frame_start(0), frame_duration(0) {} + + MetadataItem(uint32_t start, uint32_t duration, std::vector data, + const std::string& info = "") + : frame_start(start) + , frame_duration(duration) + , payload(std::move(data)) + , source_info(info) + {} + + // Frame range end (exclusive) + uint32_t frameEnd() const { return frame_start + frame_duration; } + + // Check if this item overlaps with another + bool overlaps(const MetadataItem& other) const { + return frame_start < other.frameEnd() && frameEnd() > other.frame_start; + } +}; + +/** + * Collection of metadata items indexed by frame_start + * Sorted map ensures items are in frame order + */ +using MetadataMap = std::map; + +// ============================================================================ +// Configuration Structures +// ============================================================================ + +/** + * Configuration passed to injection strategies + */ +struct InjectionConfig { + MP4Movie movie; // Movie to inject into + MP4Track videoTrack; // Reference video track + std::vector videoSampleDurations; // Video sample durations in timescale units + std::string t35Prefix; // T.35 prefix hex string (e.g., "B500900001") + + InjectionConfig() + : movie(nullptr) + , videoTrack(nullptr) + {} +}; + +/** + * Configuration passed to extraction strategies + */ +struct ExtractionConfig { + MP4Movie movie; // Movie to extract from + std::string outputPath; // Output directory or file path + std::string t35Prefix; // T.35 prefix to look for (e.g., "B500900001") + + ExtractionConfig() + : movie(nullptr) + {} +}; + +// ============================================================================ +// Error Handling +// ============================================================================ + +/** + * Error codes for T.35 tool operations + */ +enum class T35Error { + Success = 0, + InvalidJSON, + MissingFiles, + SourceError, + InjectionFailed, + ExtractionFailed, + NoMetadataFound, + ValidationFailed, + MP4Error, + NotImplemented +}; + +/** + * Exception type for T.35 tool errors + */ +class T35Exception : public std::exception { +public: + T35Exception(T35Error err, const std::string& msg) + : code(err) + , message(msg) + { + fullMessage = "T35Error(" + std::to_string(static_cast(err)) + "): " + msg; + } + + const char* what() const noexcept override { + return fullMessage.c_str(); + } + + T35Error getCode() const { return code; } + const std::string& getMessage() const { return message; } + +private: + T35Error code; + std::string message; + std::string fullMessage; +}; + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/** + * Validate a MetadataMap + * Checks for: + * - No overlapping frame ranges + * - Valid frame numbers + * - Non-empty payloads + * + * @param items The metadata map to validate + * @param errorMsg Output parameter for error message + * @return true if valid, false otherwise + */ +bool validateMetadataMap(const MetadataMap& items, std::string& errorMsg); + +/** + * Check if metadata map covers all frames (no gaps) + * + * @param items The metadata map to check + * @param totalFrames Total number of frames in video + * @return true if all frames are covered + */ +bool isFullyCovering(const MetadataMap& items, uint32_t totalFrames); + +/** + * Check if metadata map has single static entry covering all frames + * + * @param items The metadata map to check + * @param totalFrames Total number of frames in video + * @return true if single static entry + */ +bool isStaticMetadata(const MetadataMap& items, uint32_t totalFrames); + +} // namespace t35 diff --git a/IsoLib/t35_tool/common/T35Prefix.cpp b/IsoLib/t35_tool/common/T35Prefix.cpp new file mode 100644 index 00000000..79678860 --- /dev/null +++ b/IsoLib/t35_tool/common/T35Prefix.cpp @@ -0,0 +1,159 @@ +#include "T35Prefix.hpp" +#include +#include +#include +#include + +namespace t35 +{ + +T35Prefix::T35Prefix() {} + +T35Prefix::T35Prefix(const std::string &input) { parse(input); } + +T35Prefix::T35Prefix(const std::string &hexStr, const std::string &description) + : hexString(normalizeHex(hexStr)) + , desc(description) +{ +} + +bool T35Prefix::parse(const std::string &input) +{ + if(input.empty()) + { + return false; + } + + // Find colon separator + size_t colonPos = input.find(':'); + + if(colonPos == std::string::npos) + { + // No description, entire string is hex + hexString = normalizeHex(input); + desc.clear(); + } + else + { + // Split into hex and description + hexString = normalizeHex(input.substr(0, colonPos)); + desc = input.substr(colonPos + 1); + + // Validate description + if(!isValidDescription(desc)) + { + hexString.clear(); + desc.clear(); + return false; + } + } + + // Validate hex string + if(!isValidHex(hexString)) + { + hexString.clear(); + desc.clear(); + return false; + } + + return true; +} + +std::string T35Prefix::toString() const +{ + if(desc.empty()) + { + return hexString; + } + return hexString + ":" + desc; +} + +std::vector T35Prefix::toBytes() const { return hexToBytes(hexString); } + +bool T35Prefix::isValid() const { return !hexString.empty() && isValidHex(hexString); } + +// Static helper: Check if string is valid hex (uppercase, even length) +bool T35Prefix::isValidHex(const std::string &str) +{ + if(str.empty()) + { + return false; + } + + // Must be even length (pairs of hex digits) + if(str.size() % 2 != 0) + { + return false; + } + + // All characters must be uppercase hex digits (0-9, A-F) + return std::all_of(str.begin(), str.end(), [](unsigned char c) + { return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F'); }); +} + +// Static helper: Validate description (no colon, no C0 control chars) +bool T35Prefix::isValidDescription(const std::string &desc) +{ + if(desc.empty()) + { + return true; // Empty description is valid + } + + // Check for forbidden characters: + // - Colon (U+003A) + // - C0 control characters (U+0000-U+001F) + return std::none_of(desc.begin(), desc.end(), + [](unsigned char c) { return c == ':' || c <= 0x1F; }); +} + +// Static helper: Normalize hex string to uppercase +std::string T35Prefix::normalizeHex(const std::string &hex) +{ + std::string result = hex; + std::transform(result.begin(), result.end(), result.begin(), + [](unsigned char c) { return std::toupper(c); }); + return result; +} + +// Static helper: Convert hex string to bytes +std::vector T35Prefix::hexToBytes(const std::string &hex) +{ + std::vector bytes; + + if(hex.size() % 2 != 0) + { + return bytes; // Invalid, return empty + } + + bytes.reserve(hex.size() / 2); + + for(size_t i = 0; i < hex.size(); i += 2) + { + std::string byteStr = hex.substr(i, 2); + unsigned int byteVal = 0; + + std::stringstream ss; + ss << std::hex << byteStr; + ss >> byteVal; + + bytes.push_back(static_cast(byteVal)); + } + + return bytes; +} + +// Static helper: Convert bytes to hex string +std::string T35Prefix::bytesToHex(const std::vector &bytes) +{ + std::ostringstream oss; + oss << std::hex << std::uppercase; + + for(uint8_t byte : bytes) + { + oss << std::setw(2) << std::setfill('0') << static_cast(byte); + } + + return oss.str(); +} + +} // namespace t35 diff --git a/IsoLib/t35_tool/common/T35Prefix.hpp b/IsoLib/t35_tool/common/T35Prefix.hpp new file mode 100644 index 00000000..645309ee --- /dev/null +++ b/IsoLib/t35_tool/common/T35Prefix.hpp @@ -0,0 +1,143 @@ +#pragma once + +#include +#include +#include + +namespace t35 { + +/** + * T.35 Prefix representation + * Handles parsing and conversion of ITU-T T.35 message prefixes + * + * Format: T35Prefix[":" T35Description] + * + * T35Prefix consists of an even number of uppercase hexadecimal digits (0-9, A-F), + * representing the initial bytes of an ITU-T T.35 message. It includes: + * - Country code (including any extension bytes) + * - Terminal provider code + * - Terminal provider-oriented code + * + * T35Description (if present) follows the colon (U+003A) and provides a + * human-readable description. It shall not contain: + * - Colon (U+003A) + * - C0 control characters (U+0000–U+001F) + * + * IMPORTANT: T35Description is informative only and shall not be used for + * identification, matching, or processing. Message identification is based + * solely on T35Prefix hex bytes. + * + * Examples: + * "B500900001:SMPTE-ST2094-50" - With description + * "B500900001" - Without description + * + * Regex: ^[0-9A-F]{2}(?:[0-9A-F]{2})*(?::[^\x00-\x1F:]+)?$ + */ +class T35Prefix { +public: + /** + * Default constructor - empty prefix + */ + T35Prefix(); + + /** + * Construct from hex string with optional description + * @param input Format: "HEXSTRING" or "HEXSTRING:DESCRIPTION" + */ + explicit T35Prefix(const std::string& input); + + /** + * Construct from hex string and description separately + */ + T35Prefix(const std::string& hexStr, const std::string& desc); + + /** + * Parse from string format + * @param input Format: T35Prefix[":" T35Description] + * @return true if parsed successfully + * + * Note: Hex string will be normalized to uppercase + */ + bool parse(const std::string& input); + + /** + * Get hex string only (without description) + * Always uppercase as per spec + */ + const std::string& hex() const { return hexString; } + + /** + * Get description (may be empty) + * Description is informative only + */ + const std::string& description() const { return desc; } + + /** + * Get full string representation "HEX:DESCRIPTION" or "HEX" + */ + std::string toString() const; + + /** + * Convert hex string to binary bytes + */ + std::vector toBytes() const; + + /** + * Check if prefix is empty/invalid + */ + bool empty() const { return hexString.empty(); } + + /** + * Check if prefix is valid (hex string is valid hex) + */ + bool isValid() const; + + /** + * Get byte length of prefix + */ + size_t byteLength() const { return hexString.size() / 2; } + + /** + * Compare prefixes (by hex string only, ignoring description) + */ + bool operator==(const T35Prefix& other) const { + return hexString == other.hexString; + } + + bool operator!=(const T35Prefix& other) const { + return !(*this == other); + } + + /** + * Static helper: Check if string is valid hex + * Must be uppercase hex digits (0-9, A-F) and even length + */ + static bool isValidHex(const std::string& str); + + /** + * Static helper: Check if description is valid + * Must not contain colon (U+003A) or C0 control characters (U+0000-U+001F) + */ + static bool isValidDescription(const std::string& desc); + + /** + * Static helper: Normalize hex string to uppercase + */ + static std::string normalizeHex(const std::string& hex); + + /** + * Static helper: Convert hex string to bytes + */ + static std::vector hexToBytes(const std::string& hex); + + /** + * Static helper: Convert bytes to hex string + */ + static std::string bytesToHex(const std::vector& bytes); + +private: + std::string hexString; // Hex digits only (e.g., "B500900001") + std::string desc; // Optional description (e.g., "SMPTE-ST2094-50") +}; + +} // namespace t35 diff --git a/IsoLib/t35_tool/extraction/AutoExtractor.cpp b/IsoLib/t35_tool/extraction/AutoExtractor.cpp new file mode 100644 index 00000000..76b74213 --- /dev/null +++ b/IsoLib/t35_tool/extraction/AutoExtractor.cpp @@ -0,0 +1,62 @@ +#include "AutoExtractor.hpp" +#include "MebxMe4cExtractor.hpp" +#include "DedicatedIt35Extractor.hpp" +#include "SampleGroupExtractor.hpp" +#include "../common/Logger.hpp" + +namespace t35 +{ + +bool AutoExtractor::canExtract(const ExtractionConfig &config, std::string &reason) +{ + // Try extractors in priority order + std::vector> extractors; + extractors.push_back(std::make_unique()); + extractors.push_back(std::make_unique()); + extractors.push_back(std::make_unique()); + + for(const auto &extractor : extractors) + { + std::string extractorReason; + if(extractor->canExtract(config, extractorReason)) + { + LOG_INFO("Auto-detected strategy: {}", extractor->getName()); + return true; + } + } + + reason = "No compatible metadata tracks found (tried: mebx-me4c, dedicated-it35, sample-group)"; + return false; +} + +MP4Err AutoExtractor::extract(const ExtractionConfig &config, MetadataMap *outItems) +{ + LOG_INFO("Auto-detecting extraction strategy"); + + // Try extractors in priority order + std::vector> extractors; + extractors.push_back(std::make_unique()); + extractors.push_back(std::make_unique()); + extractors.push_back(std::make_unique()); + + for(auto &extractor : extractors) + { + std::string reason; + if(extractor->canExtract(config, reason)) + { + LOG_INFO("Using auto-detected strategy: {}", extractor->getName()); + return extractor->extract(config, outItems); // Pass through outItems! + } + else + { + LOG_DEBUG("Strategy '{}' cannot extract: {}", extractor->getName(), reason); + } + } + + LOG_ERROR("No compatible extraction strategy found"); + throw T35Exception( + T35Error::ExtractionFailed, + "No compatible metadata tracks found (tried: mebx-me4c, dedicated-it35, sample-group)"); +} + +} // namespace t35 diff --git a/IsoLib/t35_tool/extraction/AutoExtractor.hpp b/IsoLib/t35_tool/extraction/AutoExtractor.hpp new file mode 100644 index 00000000..1ef9b066 --- /dev/null +++ b/IsoLib/t35_tool/extraction/AutoExtractor.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "ExtractionStrategy.hpp" + +namespace t35 { + +/** + * Auto-detection extractor + * Tries all extraction strategies in priority order + */ +class AutoExtractor : public ExtractionStrategy { +public: + AutoExtractor() = default; + virtual ~AutoExtractor() = default; + + std::string getName() const override { return "auto"; } + + std::string getDescription() const override { + return "Auto-detect extraction method"; + } + + bool canExtract(const ExtractionConfig& config, + std::string& reason) override; + + MP4Err extract(const ExtractionConfig& config, MetadataMap* outItems = nullptr) override; +}; + +} // namespace t35 diff --git a/IsoLib/t35_tool/extraction/DedicatedIt35Extractor.cpp b/IsoLib/t35_tool/extraction/DedicatedIt35Extractor.cpp new file mode 100644 index 00000000..37da7e12 --- /dev/null +++ b/IsoLib/t35_tool/extraction/DedicatedIt35Extractor.cpp @@ -0,0 +1,399 @@ +#include "DedicatedIt35Extractor.hpp" +#include "../sources/SMPTE_ST2094_50.hpp" +#include "../common/Logger.hpp" +#include "../common/T35Prefix.hpp" + +extern "C" +{ +#include "MP4Movies.h" +#include "MP4Atoms.h" +#include "ISOMovies.h" +} + +#include +#include +#include + +namespace t35 +{ + +// DedicatedIt35Extractor implementation + +DedicatedIt35Extractor::~DedicatedIt35Extractor() { clearCache(); } + +void DedicatedIt35Extractor::clearCache() { m_cachedTrack = nullptr; } + +// Helper: Find dedicated IT35 metadata track with matching T.35 prefix +static MP4Err findIt35MetadataTrack(MP4Movie moov, const std::string &t35PrefixStr, + MP4Track *outTrack) +{ + MP4Err err = MP4NoErr; + *outTrack = nullptr; + + u32 trackCount = 0; + err = MP4GetMovieTrackCount(moov, &trackCount); + if(err) return err; + + LOG_DEBUG("Searching for IT35 track in {} tracks", trackCount); + + // Search for IT35 metadata track + for(u32 i = 1; i <= trackCount; ++i) + { + MP4Track trak = nullptr; + MP4Media media = nullptr; + u32 handlerType = 0; + + err = MP4GetMovieIndTrack(moov, i, &trak); + if(err) continue; + + err = MP4GetTrackMedia(trak, &media); + if(err) continue; + + err = MP4GetMediaHandlerDescription(media, &handlerType, nullptr); + if(err) continue; + + // Only metadata tracks + if(handlerType != MP4MetaHandlerType) continue; + + u32 trackID = 0; + MP4GetTrackID(trak, &trackID); + LOG_DEBUG("Found metadata track with ID {}", trackID); + + // Get first sample description + MP4Handle sampleEntryH = nullptr; + err = MP4NewHandle(0, &sampleEntryH); + if(err) continue; + + err = MP4GetMediaSampleDescription(media, 1, sampleEntryH, nullptr); + if(err) + { + MP4DisposeHandle(sampleEntryH); + continue; + } + + // Check if it's 'it35' + u32 type = 0; + err = ISOGetSampleDescriptionType(sampleEntryH, &type); + + if(err || type != MP4T35MetadataSampleEntryType) + { + MP4DisposeHandle(sampleEntryH); + continue; + } + + LOG_INFO("Found IT35 track with ID {}", trackID); + + // Read t35_identifier and description from the serialized sample entry handle + u8 *identifier = nullptr; + u32 identifierSize = 0; + char *description = nullptr; + + MP4Err readErr = + ISOGetT35SampleEntryFields(sampleEntryH, &identifier, &identifierSize, &description); + MP4DisposeHandle(sampleEntryH); + sampleEntryH = nullptr; + + if(readErr || identifier == nullptr || identifierSize == 0) + { + LOG_WARN("Could not read t35_identifier from IT35 sample entry"); + free(identifier); + free(description); + continue; + } + + // Convert t35_identifier bytes to hex string + std::string hexStr; + hexStr.reserve(identifierSize * 2); + for(u32 j = 0; j < identifierSize; j++) + { + char buf[3]; + snprintf(buf, sizeof(buf), "%02X", identifier[j]); + hexStr += buf; + } + free(identifier); + + // Build prefix string: "HEX:Description" + std::string filePrefix = hexStr; + if(description && description[0] != '\0') + { + filePrefix += ":"; + filePrefix += description; + } + + LOG_DEBUG("Parsed 'it35' sample entry: identifier={} ({} bytes), description='{}'", hexStr, + identifierSize, (description && description[0] != '\0') ? description : ""); + free(description); + + // Parse both prefixes to compare hex part only + T35Prefix requestedPrefix(t35PrefixStr); + T35Prefix filePrefixParsed(filePrefix); + + if(requestedPrefix.hex() != filePrefixParsed.hex()) + { + LOG_DEBUG("T35 hex '{}' does not match requested hex '{}'", filePrefixParsed.hex(), + requestedPrefix.hex()); + continue; + } + LOG_DEBUG("T35 hex '{}' matches requested hex '{}'", filePrefixParsed.hex(), + requestedPrefix.hex()); + + if(!requestedPrefix.description().empty() && !filePrefixParsed.description().empty() && + requestedPrefix.description() != filePrefixParsed.description()) + { + LOG_WARN("T.35 description mismatch: requested '{}' but file has '{}'", + requestedPrefix.description(), filePrefixParsed.description()); + } + + // Check for 'rndr' track reference + MP4Track videoTrack = nullptr; + err = MP4GetTrackReference(trak, MP4_FOUR_CHAR_CODE('r', 'n', 'd', 'r'), 1, &videoTrack); + if(err) + { + LOG_WARN("IT35 track ID {} has no 'rndr' track reference, continuing anyway", trackID); + } + else + { + u32 videoTrackID = 0; + MP4GetTrackID(videoTrack, &videoTrackID); + LOG_DEBUG("IT35 track references video track ID {}", videoTrackID); + } + + // Success! + *outTrack = trak; + return MP4NoErr; + } + + LOG_ERROR("No 'it35' metadata track found"); + return MP4NotFoundErr; +} + +bool DedicatedIt35Extractor::canExtract(const ExtractionConfig &config, std::string &reason) +{ + if(!config.movie) + { + reason = "No movie provided"; + return false; + } + + // Clear any previous cache + clearCache(); + + // Find and cache the track + MP4Err err = findIt35MetadataTrack(config.movie, config.t35Prefix, &m_cachedTrack); + + if(err) + { + reason = "No dedicated IT35 metadata track found"; + return false; + } + + return true; +} + +MP4Err DedicatedIt35Extractor::extract(const ExtractionConfig &config, MetadataMap *outItems) +{ + LOG_INFO("Extracting metadata using dedicated-it35 extractor"); + LOG_INFO("T.35 prefix: {}", config.t35Prefix); + if(!outItems) + { + LOG_INFO("Output path: {}", config.outputPath); + } + else + { + LOG_INFO("Output mode: in-memory"); + } + + MP4Err err = MP4NoErr; + MP4Track it35Track = nullptr; + + // Use cached track if available (from canExtract), otherwise find it now + if(m_cachedTrack) + { + LOG_DEBUG("Using cached IT35 track"); + it35Track = m_cachedTrack; + + // Clear cache so we don't reuse it by mistake + m_cachedTrack = nullptr; + } + else + { + // Fallback: extract() called without canExtract() + LOG_DEBUG("Finding IT35 track (cache not available)"); + err = findIt35MetadataTrack(config.movie, config.t35Prefix, &it35Track); + if(err) + { + LOG_ERROR("Failed to find IT35 track (err={})", err); + return err; + } + } + + // Get media and timescale + MP4Media it35Media = nullptr; + u32 timescale = 1000; // default + err = MP4GetTrackMedia(it35Track, &it35Media); + if(err != MP4NoErr) + { + LOG_ERROR("Failed to get IT35 media (err={})", err); + return err; + } + + err = MP4GetMediaTimeScale(it35Media, ×cale); + if(err != MP4NoErr) + { + LOG_WARN("Failed to get timescale, using default 1000"); + timescale = 1000; + } + LOG_DEBUG("IT35 track timescale: {}", timescale); + + // Get sample count + u32 sampleCount = 0; + err = MP4GetMediaSampleCount(it35Media, &sampleCount); + if(err != MP4NoErr) + { + LOG_ERROR("Failed to get sample count (err={})", err); + return err; + } + LOG_INFO("IT35 track has {} samples", sampleCount); + + // Create output directory + namespace fs = std::filesystem; + fs::path outDir(config.outputPath); + + if(!fs::exists(outDir)) + { + if(!fs::create_directories(outDir)) + { + LOG_ERROR("Failed to create output directory: {}", config.outputPath); + return MP4IOErr; + } + } + + LOG_INFO("Extracting samples to {}", outDir.string()); + + // Extract all samples + std::vector manifestItems; + u32 currentFrame = 0; + + for(u32 i = 1; i <= sampleCount; ++i) + { + MP4Handle sampleH = nullptr; + u32 sampleSize = 0; + u32 sampleFlags = 0; + u32 sampleDescIndex = 0; + u64 dts = 0; + u64 duration = 0; + s32 ctsOffset = 0; + + err = MP4NewHandle(0, &sampleH); + if(err) + { + LOG_ERROR("Failed to create sample handle (err={})", err); + return err; + } + + err = MP4GetIndMediaSample(it35Media, i, sampleH, &sampleSize, &dts, &ctsOffset, &duration, + &sampleFlags, &sampleDescIndex); + + if(err) + { + MP4DisposeHandle(sampleH); + LOG_ERROR("Failed to read sample {} (err={})", i, err); + return err; + } + + // Calculate frame duration (simplification - would need video track info for accurate + // calculation) + u32 frameDuration = 1; + if(duration > 0 && timescale > 0) + { + // Rough estimate: assume ~24-60fps + frameDuration = (u32)duration / (timescale / 60); + if(frameDuration == 0) frameDuration = 1; + } + + // Decode the metadata if they are SMPTE ST 2094-50 + if(config.t35Prefix == "B500900001:SMPTE-ST2094-50") + { + SMPTE_ST2094_50 st2094_50; + std::vector binaryData(sampleSize); + std::memcpy(binaryData.data(), *sampleH, sampleSize); + + st2094_50.decodeBinaryToSyntaxElements(binaryData); + st2094_50.convertSyntaxElementsToMetadataItems(); + st2094_50.dbgPrintMetadataItems(); // Print decoded metadata from bitstream + } + + // Write binary file + fs::path binFile = outDir / ("metadata_" + std::to_string(i) + ".bin"); + std::ofstream out(binFile, std::ios::binary); + if(!out) + { + LOG_ERROR("Failed to open {} for writing", binFile.string()); + MP4DisposeHandle(sampleH); + return MP4IOErr; + } + + // Decode the metadata if they are SMPTE ST 2094-50 + if(config.t35Prefix == "B500900001:SMPTE-ST2094-50") + { + SMPTE_ST2094_50 st2094_50; + std::vector binaryData(sampleSize); + std::memcpy(binaryData.data(), *sampleH, sampleSize); + + st2094_50.decodeBinaryToSyntaxElements(binaryData); + st2094_50.convertSyntaxElementsToMetadataItems(); + st2094_50.dbgPrintMetadataItems(); // Print decoded metadata from bitstream + } + + // Samples are raw payloads (no box wrapper) + out.write((char *)*sampleH, sampleSize); + out.close(); + + LOG_INFO("Extracted sample {}: {} bytes, DTS={}, duration={} (frame {})", i, sampleSize, dts, + duration, currentFrame); + + // Add to manifest + nlohmann::json item; + item["frame_start"] = currentFrame; + item["frame_duration"] = frameDuration; + item["binary_file"] = binFile.filename().string(); + item["sample_size"] = sampleSize; + item["dts"] = static_cast(dts); + item["duration_timescale"] = static_cast(duration); + manifestItems.push_back(item); + + currentFrame += frameDuration; + + MP4DisposeHandle(sampleH); + } + + LOG_INFO("Extracted {} metadata samples", sampleCount); + + // Write manifest JSON + if(!manifestItems.empty()) + { + nlohmann::json manifest; + manifest["t35_prefix"] = config.t35Prefix; + manifest["timescale"] = timescale; + manifest["sample_count"] = sampleCount; + manifest["items"] = manifestItems; + + fs::path manifestFile = outDir / "manifest.json"; + std::ofstream manifestOut(manifestFile); + if(manifestOut) + { + manifestOut << manifest.dump(2); + manifestOut.close(); + LOG_INFO("Wrote manifest to {}", manifestFile.string()); + } + else + { + LOG_WARN("Failed to write manifest file"); + } + } + + LOG_INFO("Extraction complete"); + return MP4NoErr; +} + +} // namespace t35 diff --git a/IsoLib/t35_tool/extraction/DedicatedIt35Extractor.hpp b/IsoLib/t35_tool/extraction/DedicatedIt35Extractor.hpp new file mode 100644 index 00000000..5f430628 --- /dev/null +++ b/IsoLib/t35_tool/extraction/DedicatedIt35Extractor.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include "ExtractionStrategy.hpp" + +// Forward declarations from libisomediafile +extern "C" { +struct MP4TrackRecord; +typedef struct MP4TrackRecord* MP4Track; +} + +namespace t35 { + +/** + * Dedicated IT35 metadata track extractor + * Extracts from tracks with 'it35' sample entry (T35MetadataSampleEntry) + */ +class DedicatedIt35Extractor : public ExtractionStrategy { +public: + DedicatedIt35Extractor() = default; + virtual ~DedicatedIt35Extractor(); + + std::string getName() const override { return "dedicated-it35"; } + + std::string getDescription() const override { + return "Extract from dedicated IT35 metadata track"; + } + + bool canExtract(const ExtractionConfig& config, + std::string& reason) override; + + MP4Err extract(const ExtractionConfig& config, MetadataMap* outItems = nullptr) override; + +private: + // Cache the track found in canExtract() for use in extract() + MP4Track m_cachedTrack = nullptr; + + void clearCache(); +}; + +} // namespace t35 diff --git a/IsoLib/t35_tool/extraction/ExtractionStrategy.cpp b/IsoLib/t35_tool/extraction/ExtractionStrategy.cpp new file mode 100644 index 00000000..554aff8b --- /dev/null +++ b/IsoLib/t35_tool/extraction/ExtractionStrategy.cpp @@ -0,0 +1,42 @@ +#include "ExtractionStrategy.hpp" +#include "MebxMe4cExtractor.hpp" +#include "DedicatedIt35Extractor.hpp" +#include "AutoExtractor.hpp" +#include "SampleGroupExtractor.hpp" +#include "SeiExtractor.hpp" +#include "../common/Logger.hpp" + +namespace t35 +{ + +std::unique_ptr createExtractionStrategy(const std::string &strategyName) +{ + LOG_DEBUG("Creating extraction strategy: '{}'", strategyName); + + if(strategyName == "auto") + { + return std::make_unique(); + } + else if(strategyName == "mebx-me4c") + { + return std::make_unique(); + } + else if(strategyName == "dedicated-it35") + { + return std::make_unique(); + } + else if(strategyName == "sample-group") + { + return std::make_unique(); + } + else if(strategyName == "sei") + { + return std::make_unique(); + } + else + { + throw T35Exception(T35Error::ExtractionFailed, "Unknown extraction strategy: " + strategyName); + } +} + +} // namespace t35 diff --git a/IsoLib/t35_tool/extraction/ExtractionStrategy.hpp b/IsoLib/t35_tool/extraction/ExtractionStrategy.hpp new file mode 100644 index 00000000..eafa45ba --- /dev/null +++ b/IsoLib/t35_tool/extraction/ExtractionStrategy.hpp @@ -0,0 +1,91 @@ +#pragma once + +#include "../common/MetadataTypes.hpp" +#include "../common/T35Prefix.hpp" + +// Forward declarations from libisomediafile +extern "C" { +typedef int MP4Err; +} + +#include +#include + +namespace t35 { + +/** + * Abstract interface for metadata extraction strategies + * + * An ExtractionStrategy handles: + * - Finding metadata in MP4 container (tracks, groups, etc.) + * - Reading metadata samples + * - Writing output files (binary, JSON, or video with SEI) + * + * Different strategies extract from different MP4 storage methods: + * - MEBX tracks (me4c namespace) + * - Dedicated metadata tracks + * - Sample groups + * - Sample entry boxes + * - SEI conversion (metadata → video with SEI NALs) + */ +class ExtractionStrategy { +public: + virtual ~ExtractionStrategy() = default; + + /** + * Get strategy name + * @return Name string (e.g., "mebx-me4c", "auto", "sei") + */ + virtual std::string getName() const = 0; + + /** + * Get strategy description + * @return Human-readable description + */ + virtual std::string getDescription() const = 0; + + /** + * Check if this strategy can extract from the given movie + * + * @param config Extraction configuration (movie, prefix, etc.) + * @param reason Output parameter for reason if cannot extract + * @return true if strategy can extract + */ + virtual bool canExtract(const ExtractionConfig& config, + std::string& reason) = 0; + + /** + * Extract metadata from movie + * + * @param config Configuration (movie, output path, prefix, etc.) + * @param outItems Optional output parameter for in-memory extraction. + * If non-null, metadata is returned in this map instead of writing files. + * If null (default), writes files to config.outputPath as before. + * @return MP4Err (0 = success) + * @throws T35Exception on error + * + * Output format depends on strategy and outItems parameter: + * - If outItems == nullptr: Write .bin files + manifest.json (original behavior) + * - If outItems != nullptr: Populate MetadataMap with in-memory data + * - SEI extractor: Always writes .hevc/.265 video file (ignores outItems) + */ + virtual MP4Err extract(const ExtractionConfig& config, MetadataMap* outItems = nullptr) = 0; +}; + +/** + * Factory function to create extraction strategy from name + * + * Available strategies: + * - "auto": Auto-detect (tries all strategies) + * - "mebx-me4c": MEBX track with me4c namespace + * - "dedicated-it35": Dedicated metadata track + * - "sample-group": Sample group + * - "sei": Convert metadata to video with SEI NALs + * + * @param strategyName Strategy name + * @return Unique pointer to ExtractionStrategy + * @throws T35Exception if strategy is unknown + */ +std::unique_ptr createExtractionStrategy(const std::string& strategyName); + +} // namespace t35 diff --git a/IsoLib/t35_tool/extraction/MebxMe4cExtractor.cpp b/IsoLib/t35_tool/extraction/MebxMe4cExtractor.cpp new file mode 100644 index 00000000..3b08e732 --- /dev/null +++ b/IsoLib/t35_tool/extraction/MebxMe4cExtractor.cpp @@ -0,0 +1,542 @@ +#include "MebxMe4cExtractor.hpp" +#include "../sources/SMPTE_ST2094_50.hpp" +#include "../common/Logger.hpp" +#include "../common/T35Prefix.hpp" + +extern "C" +{ +#include "MP4Movies.h" +#include "MP4Atoms.h" +} + +#include +#include +#include + +namespace t35 +{ + +// Helper: Convert u32 4CC to string representation +static std::string fourCCToString(u32 fourcc) +{ + char buf[5] = {0}; + buf[0] = (fourcc >> 24) & 0xFF; + buf[1] = (fourcc >> 16) & 0xFF; + buf[2] = (fourcc >> 8) & 0xFF; + buf[3] = fourcc & 0xFF; + return std::string(buf); +} + +// MebxMe4cExtractor implementation + +MebxMe4cExtractor::~MebxMe4cExtractor() { clearCache(); } + +void MebxMe4cExtractor::clearCache() +{ + if(m_cachedReader) + { + MP4DisposeTrackReader(m_cachedReader); + m_cachedReader = nullptr; + } + m_cachedTrack = nullptr; +} + +// Helper: Find mebx track with me4c namespace and it35 key_value +static MP4Err findMebxMe4cTrackReader(MP4Movie moov, const std::string &t35PrefixStr, + MP4TrackReader *outReader, MP4Track *outTrack) +{ + MP4Err err = MP4NoErr; + *outReader = nullptr; + if(outTrack) *outTrack = nullptr; + + u32 trackCount = 0; + err = MP4GetMovieTrackCount(moov, &trackCount); + if(err) return err; + + LOG_DEBUG("Searching for mebx me4c track in {} tracks", trackCount); + + // Search for mebx track + for(u32 i = 1; i <= trackCount; ++i) + { + MP4Track trak = nullptr; + MP4Media media = nullptr; + u32 handlerType = 0; + + err = MP4GetMovieIndTrack(moov, i, &trak); + if(err) continue; + + err = MP4GetTrackMedia(trak, &media); + if(err) continue; + + err = MP4GetMediaHandlerDescription(media, &handlerType, nullptr); + if(err) continue; + + // Only metadata tracks + if(handlerType != MP4MetaHandlerType) continue; + + u32 trackID = 0; + MP4GetTrackID(trak, &trackID); + LOG_DEBUG("Found metadata track with ID {}", trackID); + + // Create track reader + MP4TrackReader reader = nullptr; + err = MP4CreateTrackReader(trak, &reader); + if(err) continue; + + // Get sample description (we'll reuse this for both type checking and metadata config) + MP4Handle sampleEntryH = nullptr; + err = MP4NewHandle(0, &sampleEntryH); + if(err) + { + MP4DisposeTrackReader(reader); + continue; + } + + err = MP4TrackReaderGetCurrentSampleDescription(reader, sampleEntryH); + if(err) + { + MP4DisposeHandle(sampleEntryH); + MP4DisposeTrackReader(reader); + continue; + } + + // Check if it's 'mebx' + u32 type = 0; + err = ISOGetSampleDescriptionType(sampleEntryH, &type); + + if(err || type != MP4BoxedMetadataSampleEntryType) + { + MP4DisposeHandle(sampleEntryH); + MP4DisposeTrackReader(reader); + continue; + } + + LOG_INFO("Found mebx track with ID {}", trackID); + + // Check for 'rndr' track reference + MP4Track videoTrack = nullptr; + err = MP4GetTrackReference(trak, MP4_FOUR_CHAR_CODE('r', 'n', 'd', 'r'), 1, &videoTrack); + if(err) + { + LOG_WARN("Mebx track ID {} has no 'rndr' track reference, skipping", trackID); + MP4DisposeHandle(sampleEntryH); + MP4DisposeTrackReader(reader); + continue; + } + + u32 videoTrackID = 0; + MP4GetTrackID(videoTrack, &videoTrackID); + LOG_DEBUG("Mebx track references video track ID {}", videoTrackID); + + // Search for me4c namespace key with 'it35' key_value + u32 key_namespace = MP4_FOUR_CHAR_CODE('m', 'e', '4', 'c'); + MP4Handle key_value = nullptr; + err = MP4NewHandle(4, &key_value); + if(err) + { + MP4DisposeHandle(sampleEntryH); + MP4DisposeTrackReader(reader); + return err; + } + + // Key_value = 'it35' (4CC) + u32 it35_fourcc = MP4_FOUR_CHAR_CODE('i', 't', '3', '5'); + char *keyPtr = (char *)*key_value; + keyPtr[0] = (it35_fourcc >> 24) & 0xFF; + keyPtr[1] = (it35_fourcc >> 16) & 0xFF; + keyPtr[2] = (it35_fourcc >> 8) & 0xFF; + keyPtr[3] = it35_fourcc & 0xFF; + + LOG_DEBUG("Searching for key_namespace='me4c', key_value='it35' (4CC)"); + + // Iterate through all matches to find one with matching setupInfo + u32 selected_local_key_id = 0; + int found_match = 0; + + for(u32 matchIdx = 0;; matchIdx++) + { + u32 abs_idx = 0; + u32 local_key_id = 0; + + // Find the matchIdx-th entry with matching namespace + key_value + err = MP4FindMebxKeyMatchByIndex(sampleEntryH, key_namespace, key_value, matchIdx, &abs_idx, + &local_key_id); + if(err) + { + // No more matches + LOG_DEBUG("No more matches after checking {} entries", matchIdx); + break; + } + + LOG_DEBUG("Found match {}: abs_idx={}, local_key_id=0x{:08X} ('{}')", matchIdx, abs_idx, + local_key_id, fourCCToString(local_key_id)); + + // Read setupInfo for this match + MP4Handle read_key_value = nullptr; + MP4Handle setupInfoH = nullptr; + + err = MP4NewHandle(0, &read_key_value); + if(err) + { + LOG_ERROR("Failed to create read_key_value handle (err={})", err); + continue; + } + + err = MP4NewHandle(0, &setupInfoH); + if(err) + { + LOG_ERROR("Failed to create setupInfo handle (err={})", err); + MP4DisposeHandle(read_key_value); + continue; + } + + u32 read_local_key_id = 0; + u32 read_key_namespace = 0; + err = ISOGetMebxMetadataConfig(sampleEntryH, abs_idx, &read_local_key_id, &read_key_namespace, + read_key_value, nullptr, setupInfoH); + + if(err) + { + LOG_WARN("ISOGetMebxMetadataConfig failed for match {} (err={})", matchIdx, err); + MP4DisposeHandle(setupInfoH); + MP4DisposeHandle(read_key_value); + continue; + } + + LOG_DEBUG("Match {} config: local_key_id=0x{:08X}, namespace=0x{:08X}", matchIdx, + read_local_key_id, read_key_namespace); + + // Check setupInfo + u32 setupInfoSize = 0; + MP4GetHandleSize(setupInfoH, &setupInfoSize); + LOG_DEBUG(" setupInfo size = {} bytes", setupInfoSize); + + if(setupInfoSize > 0) + { + // Parse setupInfo binary format: + // 1. utf8string description (null-terminated) + // 2. unsigned int(8) t35_identifier[] (remaining bytes) + + char *setupData = (char *)*setupInfoH; + + // Read null-terminated description + size_t descLen = 0; + for(size_t i = 0; i < setupInfoSize; i++) + { + if(setupData[i] == '\0') + { + descLen = i; + break; + } + } + + std::string desc; + if(descLen > 0) + { + desc = std::string(setupData, descLen); + } + + // Read remaining bytes as t35_identifier + u32 identifierStart = descLen + 1; // Skip null terminator + u32 identifierSize = setupInfoSize - identifierStart; + + std::vector identifierBytes; + if(identifierSize > 0 && identifierStart < setupInfoSize) + { + identifierBytes.assign((uint8_t *)(setupData + identifierStart), + (uint8_t *)(setupData + setupInfoSize)); + } + + // Convert identifier bytes to hex string + std::string hexStr = T35Prefix::bytesToHex(identifierBytes); + + LOG_DEBUG(" Parsed setupInfo: description='{}', identifier={} ({} bytes)", + desc.empty() ? "(empty)" : desc, hexStr, identifierBytes.size()); + + // Create T35Prefix from parsed components + T35Prefix filePrefix(hexStr, desc); + + // Parse requested prefix + T35Prefix requestedPrefix(t35PrefixStr); + + // Verify hex prefix matches (ignore description) + if(requestedPrefix.hex() == filePrefix.hex()) + { + LOG_DEBUG("T.35 hex matches requested hex!"); + + // Warn if descriptions differ (informative only) + if(!requestedPrefix.description().empty() && !filePrefix.description().empty() && + requestedPrefix.description() != filePrefix.description()) + { + LOG_WARN("T.35 description mismatch: requested '{}' but file has '{}'", + requestedPrefix.description(), filePrefix.description()); + } + + // Found the correct match! + selected_local_key_id = local_key_id; + found_match = 1; + MP4DisposeHandle(setupInfoH); + MP4DisposeHandle(read_key_value); + break; + } + else + { + LOG_DEBUG("T.35 hex '{}' does not match requested hex '{}', continuing search", + filePrefix.hex(), requestedPrefix.hex()); + } + } + else + { + LOG_WARN("setupInfo is empty for match {}", matchIdx); + } + + MP4DisposeHandle(setupInfoH); + MP4DisposeHandle(read_key_value); + } + + MP4DisposeHandle(key_value); + + if(!found_match) + { + LOG_DEBUG("No match found with requested T.35 prefix for track {}", trackID); + MP4DisposeHandle(sampleEntryH); + MP4DisposeTrackReader(reader); + continue; + } + + // Configure the reader with the selected local_key_id + err = MP4SetMebxTrackReaderLocalKeyId(reader, selected_local_key_id); + if(err) + { + LOG_ERROR("Failed to set local_key_id (err={})", err); + MP4DisposeHandle(sampleEntryH); + MP4DisposeTrackReader(reader); + continue; + } + + LOG_INFO("Selected mebx me4c track ID {} with local_key_id = '{}' (0x{:08X})", trackID, + fourCCToString(selected_local_key_id), selected_local_key_id); + + MP4DisposeHandle(sampleEntryH); + + // Success! + *outReader = reader; + if(outTrack) *outTrack = trak; + return MP4NoErr; + } + + LOG_ERROR("No mebx track found with me4c namespace and it35 key_value"); + return MP4NotFoundErr; +} + +bool MebxMe4cExtractor::canExtract(const ExtractionConfig &config, std::string &reason) +{ + if(!config.movie) + { + reason = "No movie provided"; + return false; + } + + // Clear any previous cache + clearCache(); + + // Find and cache the reader with setupInfo verification + MP4Err err = + findMebxMe4cTrackReader(config.movie, config.t35Prefix, &m_cachedReader, &m_cachedTrack); + + if(err) + { + reason = "No mebx track with me4c namespace and matching T.35 prefix found"; + return false; + } + + return true; +} + +MP4Err MebxMe4cExtractor::extract(const ExtractionConfig &config, MetadataMap *outItems) +{ + LOG_INFO("Extracting metadata using mebx-me4c extractor"); + LOG_INFO("T.35 prefix: {}", config.t35Prefix); + if(!outItems) + { + LOG_INFO("Output path: {}", config.outputPath); + } + else + { + LOG_INFO("Output mode: in-memory"); + } + + MP4Err err = MP4NoErr; + MP4TrackReader mebxReader = nullptr; + MP4Track mebxTrack = nullptr; + + // Use cached reader if available (from canExtract), otherwise find it now + if(m_cachedReader) + { + LOG_DEBUG("Using cached mebx me4c track reader"); + mebxReader = m_cachedReader; + mebxTrack = m_cachedTrack; + + // Clear cache so we don't double-dispose + m_cachedReader = nullptr; + m_cachedTrack = nullptr; + } + else + { + // Fallback: extract() called without canExtract() + LOG_DEBUG("Finding mebx me4c track (cache not available)"); + err = findMebxMe4cTrackReader(config.movie, config.t35Prefix, &mebxReader, &mebxTrack); + if(err) + { + LOG_ERROR("Failed to find mebx me4c track (err={})", err); + return err; + } + } + + // Get timescale + MP4Media mebxMedia = nullptr; + u32 timescale = 1000; // default + err = MP4GetTrackMedia(mebxTrack, &mebxMedia); + if(err == MP4NoErr) + { + MP4GetMediaTimeScale(mebxMedia, ×cale); + } + LOG_DEBUG("Mebx track timescale: {}", timescale); + + // Create output directory + namespace fs = std::filesystem; + fs::path outDir(config.outputPath); + + if(!fs::exists(outDir)) + { + if(!fs::create_directories(outDir)) + { + LOG_ERROR("Failed to create output directory: {}", config.outputPath); + MP4DisposeTrackReader(mebxReader); + return MP4IOErr; + } + } + + LOG_INFO("Extracting samples to {}", outDir.string()); + + // Extract all samples + std::vector manifestItems; + u32 sampleCount = 0; + u32 currentFrame = 0; + + for(u32 i = 1;; ++i) + { + MP4Handle sampleH = nullptr; + u32 sampleSize = 0, sampleFlags = 0, sampleDuration = 0; + s32 dts = 0, cts = 0; + + err = MP4NewHandle(0, &sampleH); + if(err) + { + MP4DisposeTrackReader(mebxReader); + return err; + } + + err = MP4TrackReaderGetNextAccessUnitWithDuration(mebxReader, sampleH, &sampleSize, + &sampleFlags, &dts, &cts, &sampleDuration); + + if(err) + { + MP4DisposeHandle(sampleH); + if(err == MP4EOF) + { + LOG_DEBUG("Reached end of mebx samples"); + err = MP4NoErr; + break; + } + LOG_ERROR("Failed to read sample (err={})", err); + MP4DisposeTrackReader(mebxReader); + return err; + } + + sampleCount++; + + // Calculate frame duration (assuming constant frame rate) + u32 frameDuration = 1; + if(sampleDuration > 0) + { + frameDuration = sampleDuration / 512; // rough estimate + if(frameDuration == 0) frameDuration = 1; + } + + // Write binary file + fs::path binFile = outDir / ("metadata_" + std::to_string(i) + ".bin"); + std::ofstream out(binFile, std::ios::binary); + if(!out) + { + LOG_ERROR("Failed to open {} for writing", binFile.string()); + MP4DisposeHandle(sampleH); + MP4DisposeTrackReader(mebxReader); + return MP4IOErr; + } + + out.write((char *)*sampleH, sampleSize); + out.close(); + + // Decode the metadata if they are SMPTE ST 2094-50 + if(config.t35Prefix == "B500900001:SMPTE-ST2094-50") + { + SMPTE_ST2094_50 st2094_50; + std::vector binaryData(sampleSize); + std::memcpy(binaryData.data(), *sampleH, sampleSize); + + st2094_50.decodeBinaryToSyntaxElements(binaryData); + st2094_50.convertSyntaxElementsToMetadataItems(); + st2094_50.dbgPrintMetadataItems(); // Print decoded metadata from bitstream + } + + LOG_INFO("Extracted sample {}: {} bytes, DTS={}, duration={} (frame {})", i, sampleSize, dts, + sampleDuration, currentFrame); + + // Add to manifest + nlohmann::json item; + item["frame_start"] = currentFrame; + item["frame_duration"] = frameDuration; + item["binary_file"] = binFile.filename().string(); + item["sample_size"] = sampleSize; + item["dts"] = dts; + item["duration_timescale"] = sampleDuration; + manifestItems.push_back(item); + + currentFrame += frameDuration; + + MP4DisposeHandle(sampleH); + } + + MP4DisposeTrackReader(mebxReader); + + LOG_INFO("Extracted {} metadata samples", sampleCount); + + // Write manifest JSON + if(!manifestItems.empty()) + { + nlohmann::json manifest; + manifest["t35_prefix"] = config.t35Prefix; + manifest["timescale"] = timescale; + manifest["sample_count"] = sampleCount; + manifest["items"] = manifestItems; + + fs::path manifestFile = outDir / "manifest.json"; + std::ofstream manifestOut(manifestFile); + if(manifestOut) + { + manifestOut << manifest.dump(2); + manifestOut.close(); + LOG_INFO("Wrote manifest to {}", manifestFile.string()); + } + else + { + LOG_WARN("Failed to write manifest file"); + } + } + + LOG_INFO("Extraction complete"); + return MP4NoErr; +} + +} // namespace t35 diff --git a/IsoLib/t35_tool/extraction/MebxMe4cExtractor.hpp b/IsoLib/t35_tool/extraction/MebxMe4cExtractor.hpp new file mode 100644 index 00000000..26e269ba --- /dev/null +++ b/IsoLib/t35_tool/extraction/MebxMe4cExtractor.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include "ExtractionStrategy.hpp" + +// Forward declarations from libisomediafile +extern "C" { +struct MP4TrackReaderRecord; +typedef struct MP4TrackReaderRecord* MP4TrackReader; +struct MP4TrackRecord; +typedef struct MP4TrackRecord* MP4Track; +} + +namespace t35 { + +/** + * MEBX track with me4c namespace extractor + * Extracts from mebx tracks using me4c namespace with 'it35' key_value + */ +class MebxMe4cExtractor : public ExtractionStrategy { +public: + MebxMe4cExtractor() = default; + virtual ~MebxMe4cExtractor(); + + std::string getName() const override { return "mebx-me4c"; } + + std::string getDescription() const override { + return "Extract from MEBX metadata track with me4c namespace"; + } + + bool canExtract(const ExtractionConfig& config, + std::string& reason) override; + + MP4Err extract(const ExtractionConfig& config, MetadataMap* outItems = nullptr) override; + +private: + // Cache the reader and track found in canExtract() for use in extract() + MP4TrackReader m_cachedReader = nullptr; + MP4Track m_cachedTrack = nullptr; + + void clearCache(); +}; + +} // namespace t35 diff --git a/IsoLib/t35_tool/extraction/SampleGroupExtractor.cpp b/IsoLib/t35_tool/extraction/SampleGroupExtractor.cpp new file mode 100644 index 00000000..21b35aeb --- /dev/null +++ b/IsoLib/t35_tool/extraction/SampleGroupExtractor.cpp @@ -0,0 +1,361 @@ +#include "SampleGroupExtractor.hpp" +#include "../sources/SMPTE_ST2094_50.hpp" +#include "../common/Logger.hpp" +#include "../common/T35Prefix.hpp" + +extern "C" +{ +#include "MP4Movies.h" +#include "MP4Atoms.h" +} + +#include +#include +#include +#include + +namespace t35 +{ + +// Helper: Find first video track +static MP4Err findFirstVideoTrack(MP4Movie moov, MP4Track *outTrack) +{ + MP4Err err = MP4NoErr; + *outTrack = nullptr; + + u32 trackCount = 0; + err = MP4GetMovieTrackCount(moov, &trackCount); + if(err) return err; + + for(u32 i = 1; i <= trackCount; ++i) + { + MP4Track trak = nullptr; + MP4Media media = nullptr; + u32 handlerType = 0; + + err = MP4GetMovieIndTrack(moov, i, &trak); + if(err) continue; + + err = MP4GetTrackMedia(trak, &media); + if(err) continue; + + err = MP4GetMediaHandlerDescription(media, &handlerType, nullptr); + if(err) continue; + + if(handlerType == MP4VisualHandlerType) + { + *outTrack = trak; + return MP4NoErr; + } + } + + return MP4NotFoundErr; +} + +bool SampleGroupExtractor::canExtract(const ExtractionConfig &config, std::string &reason) +{ + if(!config.movie) + { + reason = "No movie provided"; + return false; + } + + MP4Err err = MP4NoErr; + + // Find video track + MP4Track videoTrack = nullptr; + err = findFirstVideoTrack(config.movie, &videoTrack); + if(err != MP4NoErr) + { + reason = "No video track found"; + return false; + } + + // Get video media + MP4Media videoMedia = nullptr; + err = MP4GetTrackMedia(videoTrack, &videoMedia); + if(err != MP4NoErr) + { + reason = "Failed to get video track media"; + return false; + } + + // Check if there are any it35 sample groups + u32 it35_sg_cnt = 0; + err = ISOGetGroupDescriptionEntryCount(videoMedia, MP4T35SampleGroupEntry, &it35_sg_cnt); + if(err != MP4NoErr || it35_sg_cnt == 0) + { + reason = "No it35 sample groups found in video track"; + return false; + } + + LOG_DEBUG("Found {} it35 sample group description(s)", it35_sg_cnt); + return true; +} + +MP4Err SampleGroupExtractor::extract(const ExtractionConfig &config, MetadataMap *outItems) +{ + LOG_INFO("Extracting metadata using sample-group extractor"); + LOG_INFO("T.35 prefix: {}", config.t35Prefix); + if(!outItems) + { + LOG_INFO("Output path: {}", config.outputPath); + } + else + { + LOG_INFO("Output mode: in-memory"); + } + + MP4Err err = MP4NoErr; + + // Find video track + MP4Track videoTrack = nullptr; + err = findFirstVideoTrack(config.movie, &videoTrack); + if(err != MP4NoErr) + { + LOG_ERROR("Failed to find video track (err={})", err); + return err; + } + + u32 videoTrackID = 0; + MP4GetTrackID(videoTrack, &videoTrackID); + LOG_INFO("Using video track ID {}", videoTrackID); + + // Get video media + MP4Media videoMedia = nullptr; + err = MP4GetTrackMedia(videoTrack, &videoMedia); + if(err != MP4NoErr) + { + LOG_ERROR("Failed to get video track media (err={})", err); + return err; + } + + // Get timescale + u32 timescale = 1000; // default + err = MP4GetMediaTimeScale(videoMedia, ×cale); + if(err != MP4NoErr) + { + LOG_WARN("Failed to get timescale, using default 1000"); + timescale = 1000; + } + LOG_DEBUG("Video track timescale: {}", timescale); + + // Get sample count + u32 sampleCount = 0; + err = MP4GetMediaSampleCount(videoMedia, &sampleCount); + if(err != MP4NoErr) + { + LOG_ERROR("Failed to get sample count (err={})", err); + return err; + } + LOG_INFO("Video track has {} samples", sampleCount); + + // Get it35 sample group count + u32 it35_sg_cnt = 0; + err = ISOGetGroupDescriptionEntryCount(videoMedia, MP4T35SampleGroupEntry, &it35_sg_cnt); + if(err != MP4NoErr || it35_sg_cnt == 0) + { + LOG_ERROR("No it35 sample groups found (err={})", err); + return MP4NotFoundErr; + } + LOG_INFO("Found {} it35 sample group description(s)", it35_sg_cnt); + + // Prepare output directory if writing to files + namespace fs = std::filesystem; + fs::path outDir; + if(!outItems) + { + outDir = config.outputPath; + if(!fs::exists(outDir)) + { + if(!fs::create_directories(outDir)) + { + LOG_ERROR("Failed to create output directory: {}", config.outputPath); + return MP4IOErr; + } + } + LOG_INFO("Extracting samples to {}", outDir.string()); + } + + // Extract each group description into memory first + MetadataMap items; + std::vector manifestItems; + + for(u32 groupIdx = 1; groupIdx <= it35_sg_cnt; ++groupIdx) + { + // Get group description + MP4Handle entryH = nullptr; + err = MP4NewHandle(0, &entryH); + if(err != MP4NoErr) + { + LOG_ERROR("Failed to create handle (err={})", err); + return err; + } + + err = ISOGetGroupDescription(videoMedia, MP4T35SampleGroupEntry, groupIdx, entryH); + if(err != MP4NoErr) + { + MP4DisposeHandle(entryH); + LOG_ERROR("Failed to get group description {} (err={})", groupIdx, err); + return err; + } + + u32 descSize = 0; + MP4GetHandleSize(entryH, &descSize); + + if(descSize < 1) + { + MP4DisposeHandle(entryH); + LOG_ERROR("Invalid T.35 group description size: {}", descSize); + return MP4BadDataErr; + } + + // First byte is complete_message_flag (bit 1) + reserved (bit 7) + u8 prefixByte = (*entryH)[0]; + bool completeMessage = (prefixByte & 0x80) != 0; + + LOG_DEBUG("Group {}: size={}, complete_message_flag={}", groupIdx, descSize, completeMessage); + + // T.35 payload is after the prefix byte + u32 payloadSize = descSize - 1; + const u8 *payloadData = ((u8 *)*entryH) + 1; + + // Copy payload to memory + std::vector payload(payloadData, payloadData + payloadSize); + + // Decode the metadata if they are SMPTE ST 2094-50 + if(config.t35Prefix == "B500900001:SMPTE-ST2094-50") + { + SMPTE_ST2094_50 st2094_50; + st2094_50.decodeBinaryToSyntaxElements(payload); + st2094_50.convertSyntaxElementsToMetadataItems(); + st2094_50.dbgPrintMetadataItems(); + } + + LOG_INFO("Extracted group {}: {} bytes (complete_message={})", groupIdx, payloadSize, + completeMessage); + + // Get which samples use this group + u32 *sampleNumbers = nullptr; + u32 samplesInGroup = 0; + err = ISOGetSampleGroupSampleNumbers(videoMedia, MP4T35SampleGroupEntry, groupIdx, + &sampleNumbers, &samplesInGroup); + + if(err == MP4NoErr && samplesInGroup > 0) + { + LOG_DEBUG("Group {} is used by {} sample(s)", groupIdx, samplesInGroup); + + // For metadata map, use the first sample as frame_start + u32 frameStart = sampleNumbers[0] - 1; // Convert to 0-based + u32 frameDuration = samplesInGroup; + + // Create metadata item + std::string sourceInfo = "metadata_" + std::to_string(groupIdx) + ".bin"; + MetadataItem item(frameStart, frameDuration, std::move(payload), sourceInfo); + items[frameStart] = item; + + // Build manifest item for file output + if(!outItems) + { + nlohmann::json manifestItem; + manifestItem["frame_start"] = frameStart; + manifestItem["frame_duration"] = frameDuration; + manifestItem["binary_file"] = sourceInfo; + manifestItem["sample_size"] = payloadSize; + manifestItem["group_index"] = groupIdx; + manifestItem["complete_message"] = completeMessage; + manifestItem["samples_in_group"] = samplesInGroup; + + // Include sample numbers for reference + nlohmann::json samplesArray = nlohmann::json::array(); + for(u32 i = 0; i < samplesInGroup; ++i) + { + samplesArray.push_back(sampleNumbers[i]); + } + manifestItem["sample_numbers"] = samplesArray; + + manifestItems.push_back(manifestItem); + } + + free(sampleNumbers); + } + else + { + LOG_WARN("Group {} has no samples mapped (err={})", groupIdx, err); + + // Still add to items but with zero duration + std::string sourceInfo = "metadata_" + std::to_string(groupIdx) + ".bin"; + MetadataItem item(0, 0, std::move(payload), sourceInfo); + items[0] = item; + + // Build manifest item for file output + if(!outItems) + { + nlohmann::json manifestItem; + manifestItem["frame_start"] = 0; + manifestItem["frame_duration"] = 0; + manifestItem["binary_file"] = sourceInfo; + manifestItem["sample_size"] = payloadSize; + manifestItem["group_index"] = groupIdx; + manifestItem["complete_message"] = completeMessage; + manifestItem["samples_in_group"] = 0; + manifestItem["sample_numbers"] = nlohmann::json::array(); + + manifestItems.push_back(manifestItem); + } + } + + MP4DisposeHandle(entryH); + } + + LOG_INFO("Extracted {} it35 group descriptions", it35_sg_cnt); + + // If in-memory mode, return items + if(outItems) + { + *outItems = std::move(items); + return MP4NoErr; + } + + // Otherwise write files + for(const auto &[frameStart, item] : items) + { + fs::path binFile = outDir / item.source_info; + std::ofstream out(binFile, std::ios::binary); + if(!out) + { + LOG_ERROR("Failed to open {} for writing", binFile.string()); + return MP4IOErr; + } + out.write((const char *)item.payload.data(), item.payload.size()); + out.close(); + } + + // Write manifest JSON + if(!manifestItems.empty()) + { + nlohmann::json manifest; + manifest["t35_prefix"] = config.t35Prefix; + manifest["timescale"] = timescale; + manifest["group_count"] = it35_sg_cnt; + manifest["items"] = manifestItems; + + fs::path manifestFile = outDir / "manifest.json"; + std::ofstream manifestOut(manifestFile); + if(manifestOut) + { + manifestOut << manifest.dump(2); + manifestOut.close(); + LOG_INFO("Wrote manifest to {}", manifestFile.string()); + } + else + { + LOG_WARN("Failed to write manifest file"); + } + } + + LOG_INFO("Extraction complete"); + return MP4NoErr; +} + +} // namespace t35 diff --git a/IsoLib/t35_tool/extraction/SampleGroupExtractor.hpp b/IsoLib/t35_tool/extraction/SampleGroupExtractor.hpp new file mode 100644 index 00000000..85973cb3 --- /dev/null +++ b/IsoLib/t35_tool/extraction/SampleGroupExtractor.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "ExtractionStrategy.hpp" + +namespace t35 { + +/** + * Sample Group extractor + * Extracts T.35 metadata from video track sample groups (sgpd/sbgp) + */ +class SampleGroupExtractor : public ExtractionStrategy { +public: + SampleGroupExtractor() = default; + virtual ~SampleGroupExtractor() = default; + + std::string getName() const override { return "sample-group"; } + + std::string getDescription() const override { + return "Extract T.35 metadata from video track sample groups"; + } + + bool canExtract(const ExtractionConfig& config, + std::string& reason) override; + + MP4Err extract(const ExtractionConfig& config, MetadataMap* outItems = nullptr) override; +}; + +} // namespace t35 diff --git a/IsoLib/t35_tool/extraction/SeiExtractor.cpp b/IsoLib/t35_tool/extraction/SeiExtractor.cpp new file mode 100644 index 00000000..01245ed3 --- /dev/null +++ b/IsoLib/t35_tool/extraction/SeiExtractor.cpp @@ -0,0 +1,462 @@ +#include "SeiExtractor.hpp" +#include "AutoExtractor.hpp" +#include "../common/Logger.hpp" +#include "../common/T35Prefix.hpp" + +extern "C" +{ +#include "MP4Movies.h" +} + +#include +#include +#include +#include +#include + +namespace t35 +{ + +namespace +{ + +/** + * Write NAL unit with Annex-B start code + */ +void writeAnnexBNAL(std::ofstream &out, const uint8_t *data, uint32_t size) +{ + static const uint8_t startCode[4] = {0x00, 0x00, 0x00, 0x01}; + out.write(reinterpret_cast(startCode), 4); + out.write(reinterpret_cast(data), size); +} + +/** + * Build HEVC SEI NAL unit with T.35 metadata + * + * @param payload T.35 metadata payload (without prefix) + * @param size Payload size in bytes + * @param t35PrefixHex T.35 prefix hex string (e.g., "B500900001") + * @return SEI NAL unit bytes + */ +std::vector buildSeiNalu(const uint8_t *payload, uint32_t size, + const std::string &t35PrefixHex) +{ + std::vector sei; + + // NAL header: forbidden_zero_bit=0, nal_unit_type=39 (prefix SEI), + // nuh_layer_id=0, nuh_temporal_id_plus1=1 + sei.push_back(0x00 | (39 << 1) | 0); + sei.push_back(0x01); + + // Extract hex portion of t35PrefixHex (strip description after ':' if present) + std::string hexOnly = t35PrefixHex; + size_t colonPos = t35PrefixHex.find(':'); + if(colonPos != std::string::npos) + { + hexOnly = t35PrefixHex.substr(0, colonPos); + } + + // Build full T.35 payload = [prefix][metadata] + // Convert hex string to binary data + std::vector prefixBytes; + if(hexOnly.size() % 2 != 0) + { + Logger::error("Invalid hex string length in T.35 prefix (must be even): " + hexOnly); + return sei; // return incomplete NAL + } + for(size_t i = 0; i < hexOnly.size(); i += 2) + { + unsigned int byteVal = 0; + std::string byteStr = hexOnly.substr(i, 2); + if(sscanf(byteStr.c_str(), "%02x", &byteVal) != 1) + { + Logger::error("Invalid hex substring in T.35 prefix: " + byteStr); + return sei; // return incomplete NAL + } + prefixBytes.push_back(static_cast(byteVal)); + } + uint32_t prefixSize = static_cast(prefixBytes.size()); + + std::vector fullPayload(prefixSize + size); + std::memcpy(fullPayload.data(), prefixBytes.data(), prefixSize); + std::memcpy(fullPayload.data() + prefixSize, payload, size); + + // payloadType = 4 (user_data_registered_itu_t_t35) + sei.push_back(4); + + // payloadSize (in one byte for simplicity, assumes < 255) + sei.push_back(static_cast(fullPayload.size())); + + // payload with emulation prevention + for(size_t i = 0; i < fullPayload.size(); i++) + { + uint8_t b = fullPayload[i]; + sei.push_back(b); + size_t n = sei.size(); + if(n >= 3 && sei[n - 1] <= 0x03 && sei[n - 2] == 0x00 && sei[n - 3] == 0x00) + { + sei.push_back(0x03); + } + } + + // rbsp_trailing_bits (10000000) + sei.push_back(0x80); + + return sei; +} + +/** + * Find first video track in movie + */ +MP4Err findFirstVideoTrack(MP4Movie movie, MP4Track *outTrack) +{ + MP4Err err = MP4NoErr; + u32 trackCount = 0; + + err = MP4GetMovieTrackCount(movie, &trackCount); + if(err) return err; + + for(u32 i = 1; i <= trackCount; i++) + { + MP4Track track = nullptr; + err = MP4GetMovieTrack(movie, i, &track); + if(err) continue; + + u32 handlerType = 0; + MP4Media media = nullptr; + err = MP4GetTrackMedia(track, &media); + if(err) continue; + + err = MP4GetMediaHandlerDescription(media, &handlerType, nullptr); + if(err) continue; + + if(handlerType == MP4VisualHandlerType) + { + *outTrack = track; + return MP4NoErr; + } + } + + return MP4NotFoundErr; +} + +/** + * Get timescale from video track + */ +MP4Err getVideoTimescale(MP4Track videoTrack, u32 *timescale) +{ + MP4Err err = MP4NoErr; + MP4Media media = nullptr; + + err = MP4GetTrackMedia(videoTrack, &media); + if(err) return err; + + err = MP4GetMediaTimeScale(media, timescale); + return err; +} + +/** + * Get total video sample count + */ +MP4Err getVideoSampleCount(MP4Track videoTrack, u32 *sampleCount) +{ + MP4Err err = MP4NoErr; + MP4Media media = nullptr; + + err = MP4GetTrackMedia(videoTrack, &media); + if(err) return err; + + err = MP4GetMediaSampleCount(media, sampleCount); + return err; +} + +} // anonymous namespace + +bool SeiExtractor::canExtract(const ExtractionConfig &config, std::string &reason) +{ + // Check if movie has video track + MP4Track videoTrack = nullptr; + MP4Err err = findFirstVideoTrack(config.movie, &videoTrack); + if(err != MP4NoErr) + { + reason = "No video track found in movie"; + return false; + } + + // Check if video track is HEVC + MP4Media media = nullptr; + err = MP4GetTrackMedia(videoTrack, &media); + if(err != MP4NoErr) + { + reason = "Cannot get media from video track"; + return false; + } + + // Try to get sample entry and check if it's HEVC + MP4Handle sampleEntryH = nullptr; + err = MP4NewHandle(0, &sampleEntryH); + if(err != MP4NoErr) + { + reason = "Cannot create handle for sample entry"; + return false; + } + + err = MP4GetMediaSampleDescription(media, 1, sampleEntryH, nullptr); + if(err != MP4NoErr) + { + MP4DisposeHandle(sampleEntryH); + reason = "Cannot get sample description"; + return false; + } + + // Try to extract HEVC NAL units to verify it's HEVC + MP4Handle hevcNALs = nullptr; + err = MP4NewHandle(0, &hevcNALs); + if(err != MP4NoErr) + { + MP4DisposeHandle(sampleEntryH); + reason = "Cannot create handle for HEVC NALs"; + return false; + } + + err = ISOGetHEVCNALUs(sampleEntryH, hevcNALs, 0); + MP4DisposeHandle(hevcNALs); + MP4DisposeHandle(sampleEntryH); + + if(err != MP4NoErr) + { + reason = "Could not get Sample Entry NALUs from HEVC video track (not HEVC?)"; + return false; + } + + // Check if there's any metadata (try auto-detection) + AutoExtractor autoExtractor; + if(!autoExtractor.canExtract(config, reason)) + { + reason = "No T.35 metadata found in movie"; + return false; + } + + return true; +} + +MP4Err SeiExtractor::extract(const ExtractionConfig &config, MetadataMap *outItems) +{ + // Note: SeiExtractor always writes to video file, outItems is ignored + (void)outItems; + + MP4Err err = MP4NoErr; + + Logger::info("Extracting T.35 metadata and converting to HEVC video with SEI NAL units"); + + // Step 1: Extract metadata directly to memory using auto-detection + Logger::info("Reading metadata from container..."); + + MetadataMap metadataItems; + AutoExtractor autoExtractor; + + // Get items directly in memory - no temp files! + err = autoExtractor.extract(config, &metadataItems); + if(err != MP4NoErr) + { + Logger::error("Failed to extract metadata"); + return err; + } + + Logger::info("Loaded " + std::to_string(metadataItems.size()) + " metadata items"); + + // Step 2: Find video track and get configuration + MP4Track videoTrack = nullptr; + err = findFirstVideoTrack(config.movie, &videoTrack); + if(err != MP4NoErr) + { + Logger::error("No video track found"); + return err; + } + + MP4Media videoMedia = nullptr; + err = MP4GetTrackMedia(videoTrack, &videoMedia); + if(err != MP4NoErr) + { + Logger::error("Cannot get video media"); + return err; + } + + // Get video timescale + u32 timescale = 0; + err = getVideoTimescale(videoTrack, ×cale); + if(err != MP4NoErr) + { + Logger::error("Cannot get video timescale"); + return err; + } + Logger::info("Video timescale: " + std::to_string(timescale)); + + // Step 3: Extract HEVC decoder configuration + MP4Handle sampleEntryH = nullptr; + err = MP4NewHandle(0, &sampleEntryH); + if(err != MP4NoErr) return err; + + err = MP4GetMediaSampleDescription(videoMedia, 1, sampleEntryH, nullptr); + if(err != MP4NoErr) + { + MP4DisposeHandle(sampleEntryH); + return err; + } + + MP4Handle hevcNALs = nullptr; + err = MP4NewHandle(0, &hevcNALs); + if(err != MP4NoErr) + { + MP4DisposeHandle(sampleEntryH); + return err; + } + + err = ISOGetHEVCNALUs(sampleEntryH, hevcNALs, 0); + if(err != MP4NoErr) + { + Logger::error("Failed to extract HEVC NAL units from sample entry"); + MP4DisposeHandle(hevcNALs); + MP4DisposeHandle(sampleEntryH); + return err; + } + + // Get NAL unit length size + u32 lengthSize = 0; + err = ISOGetNALUnitLength(sampleEntryH, &lengthSize); + MP4DisposeHandle(sampleEntryH); + if(err != MP4NoErr) + { + Logger::error("Failed to get NAL unit length size"); + MP4DisposeHandle(hevcNALs); + return err; + } + Logger::info("HEVC NAL unit length size: " + std::to_string(lengthSize)); + + // Step 4: Open output file and write decoder configuration + std::filesystem::path outputPath = config.outputPath; + if(outputPath.extension() != ".hevc" && outputPath.extension() != ".265") + { + outputPath.replace_extension(".265"); + } + + std::ofstream outFile(outputPath, std::ios::binary); + if(!outFile) + { + Logger::error("Failed to open output file: " + outputPath.string()); + MP4DisposeHandle(hevcNALs); + return MP4IOErr; + } + + // Write decoder config NALs + u32 hevcNALsSize = 0; + MP4GetHandleSize(hevcNALs, &hevcNALsSize); + outFile.write(reinterpret_cast(*hevcNALs), hevcNALsSize); + MP4DisposeHandle(hevcNALs); + + Logger::info("Wrote " + std::to_string(hevcNALsSize) + " bytes decoder configuration"); + + // Step 5: Iterate video samples and insert SEI + u32 videoSampleCount = 0; + err = getVideoSampleCount(videoTrack, &videoSampleCount); + if(err != MP4NoErr) + { + Logger::error("Cannot get video sample count"); + outFile.close(); + return err; + } + + Logger::info("Processing " + std::to_string(videoSampleCount) + " video samples"); + + // Track metadata state (sample-aligned) + auto metadataIter = metadataItems.begin(); + u64 metadataRemain = 0; // Remaining duration in timescale units + const MetadataItem *currentMetadata = nullptr; + + for(u32 sampleNum = 1; sampleNum <= videoSampleCount; sampleNum++) + { + // Get video sample + MP4Handle videoSampleH = nullptr; + u32 videoSize = 0; + u64 videoDTS = 0; + s32 videoCTSOffset = 0; + u64 videoDuration = 0; + u32 videoFlags = 0; + u32 videoDescIndex = 0; + + err = MP4NewHandle(0, &videoSampleH); + if(err != MP4NoErr) break; + + err = MP4GetIndMediaSample(videoMedia, sampleNum, videoSampleH, &videoSize, &videoDTS, + &videoCTSOffset, &videoDuration, &videoFlags, &videoDescIndex); + if(err != MP4NoErr) + { + MP4DisposeHandle(videoSampleH); + break; + } + + // Check if we need to fetch next metadata item + if(metadataRemain == 0 && metadataIter != metadataItems.end()) + { + currentMetadata = &metadataIter->second; + metadataRemain = static_cast(currentMetadata->frame_duration) * timescale / + (timescale / 1000); // Convert frames to timescale units (approximation) + ++metadataIter; + } + + // If metadata is active for this sample, write SEI NAL + if(metadataRemain > 0 && currentMetadata) + { + std::vector sei = + buildSeiNalu(currentMetadata->payload.data(), + static_cast(currentMetadata->payload.size()), config.t35Prefix); + writeAnnexBNAL(outFile, sei.data(), static_cast(sei.size())); + + if(metadataRemain > videoDuration) + { + metadataRemain -= videoDuration; + } + else + { + metadataRemain = 0; + } + } + + // Convert video sample from length-prefix to Annex-B format + uint8_t *src = reinterpret_cast(*videoSampleH); + uint8_t *end = src + videoSize; + + while(src + lengthSize <= end) + { + // Read NAL length + u32 nalLen = 0; + for(u32 i = 0; i < lengthSize; i++) + { + nalLen = (nalLen << 8) | src[i]; + } + src += lengthSize; + + // Write NAL with start code + if(src + nalLen <= end) + { + writeAnnexBNAL(outFile, src, nalLen); + src += nalLen; + } + else + { + Logger::warn("Invalid NAL length at sample " + std::to_string(sampleNum)); + break; + } + } + + MP4DisposeHandle(videoSampleH); + } + + outFile.close(); + + Logger::info("Finished writing " + outputPath.string()); + + return MP4NoErr; +} + +} // namespace t35 diff --git a/IsoLib/t35_tool/extraction/SeiExtractor.hpp b/IsoLib/t35_tool/extraction/SeiExtractor.hpp new file mode 100644 index 00000000..0730f8e5 --- /dev/null +++ b/IsoLib/t35_tool/extraction/SeiExtractor.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include "ExtractionStrategy.hpp" + +namespace t35 { + +/** + * SEI extractor for HEVC + * Extracts T.35 metadata from any container method and converts to HEVC video with SEI NAL units + * + * This strategy: + * - Auto-detects metadata storage method (MEBX, sample groups, etc.) + * - Reads video track and decoder configuration + * - Inserts SEI NAL units (user_data_registered_itu_t_t35) before video samples + * - Converts video from length-prefix format to Annex-B format with start codes + * - Outputs to .265 file + * + * If metadata spans multiple video samples, the same SEI is written for each video sample + * (redundant metadata for sample alignment) + */ +class SeiExtractor : public ExtractionStrategy { +public: + SeiExtractor() = default; + virtual ~SeiExtractor() = default; + + std::string getName() const override { return "sei"; } + + std::string getDescription() const override { + return "Extract metadata and convert to HEVC video with SEI NAL units"; + } + + bool canExtract(const ExtractionConfig& config, + std::string& reason) override; + + MP4Err extract(const ExtractionConfig& config, MetadataMap* outItems = nullptr) override; +}; + +} // namespace t35 diff --git a/IsoLib/t35_tool/injection/DedicatedIt35Strategy.cpp b/IsoLib/t35_tool/injection/DedicatedIt35Strategy.cpp new file mode 100644 index 00000000..a872ce6e --- /dev/null +++ b/IsoLib/t35_tool/injection/DedicatedIt35Strategy.cpp @@ -0,0 +1,354 @@ +#include "DedicatedIt35Strategy.hpp" +#include "../common/Logger.hpp" + +extern "C" +{ +#include "MP4Movies.h" +#include "MP4Atoms.h" +} + +#include +#include + +namespace t35 +{ + +// Helper: Find first video track +static MP4Err findFirstVideoTrack(MP4Movie moov, MP4Track *outTrack) +{ + MP4Err err = MP4NoErr; + u32 trackCount = 0; + *outTrack = nullptr; + + err = MP4GetMovieTrackCount(moov, &trackCount); + if(err) return err; + + MP4Track firstVideo = nullptr; + u32 videoCount = 0; + + for(u32 i = 1; i <= trackCount; ++i) + { + MP4Track trak = nullptr; + MP4Media media = nullptr; + u32 handlerType = 0; + + err = MP4GetMovieIndTrack(moov, i, &trak); + if(err) continue; + + err = MP4GetTrackMedia(trak, &media); + if(err) continue; + + err = MP4GetMediaHandlerDescription(media, &handlerType, nullptr); + if(err) continue; + + if(handlerType == MP4VisualHandlerType) + { + if(!firstVideo) + { + firstVideo = trak; + } + ++videoCount; + } + } + + if(!firstVideo) + { + LOG_ERROR("No video track found in movie"); + return MP4NotFoundErr; + } + + if(videoCount > 1) + { + LOG_WARN("Found {} video tracks, using the first one", videoCount); + } + + *outTrack = firstVideo; + return MP4NoErr; +} + +// Helper: Get video sample durations +static MP4Err getVideoSampleDurations(MP4Media mediaV, std::vector &durations) +{ + MP4Err err = MP4NoErr; + u32 sampleCount = 0; + + durations.clear(); + + err = MP4GetMediaSampleCount(mediaV, &sampleCount); + if(err) return err; + + durations.reserve(sampleCount); + + for(u32 i = 1; i <= sampleCount; ++i) + { + MP4Handle sampleH = nullptr; + u32 outSize, outSampleFlags, outSampleDescIndex; + u64 outDTS, outDuration; + s32 outCTSOffset; + + MP4NewHandle(0, &sampleH); + err = MP4GetIndMediaSample(mediaV, i, sampleH, &outSize, &outDTS, &outCTSOffset, &outDuration, + &outSampleFlags, &outSampleDescIndex); + if(err) + { + if(sampleH) MP4DisposeHandle(sampleH); + return err; + } + + durations.push_back(static_cast(outDuration)); + + if(sampleH) MP4DisposeHandle(sampleH); + } + + LOG_DEBUG("Collected {} video sample durations", durations.size()); + return MP4NoErr; +} + +// Helper: Build metadata durations and sizes +static MP4Err buildMetadataDurationsAndSizes(const MetadataMap &items, + const std::vector &videoDurations, + std::vector &metadataDurations, + std::vector &metadataSizes, + std::vector &sortedItems) +{ + metadataDurations.clear(); + metadataSizes.clear(); + sortedItems.clear(); + + if(items.empty()) + { + return MP4NoErr; + } + + // Sort by frame number + sortedItems.reserve(items.size()); + for(const auto &kv : items) + { + sortedItems.push_back(kv.second); + } + std::sort(sortedItems.begin(), sortedItems.end(), [](const MetadataItem &a, const MetadataItem &b) + { return a.frame_start < b.frame_start; }); + + // Build durations and sizes + metadataDurations.reserve(sortedItems.size()); + metadataSizes.reserve(sortedItems.size()); + + for(const auto &item : sortedItems) + { + // Frame numbers are 0-based + if(item.frame_start >= videoDurations.size()) + { + LOG_ERROR("Frame number {} out of range (max {})", item.frame_start, + videoDurations.size() - 1); + return MP4BadParamErr; + } + + u32 duration = videoDurations[item.frame_start]; + metadataDurations.push_back(duration); + metadataSizes.push_back(static_cast(item.payload.size())); + } + + return MP4NoErr; +} + +// Helper: Build sample data (just payloads, no box wrapper) +static MP4Err buildSampleData(const std::vector &sortedItems, + const std::vector &metadataSizes, MP4Handle *outSampleDataH) +{ + MP4Err err = MP4NoErr; + *outSampleDataH = nullptr; + + if(sortedItems.empty()) + { + return MP4NoErr; + } + + // Calculate total size (just payloads, no box wrapper) + u64 totalSize = 0; + for(u32 size : metadataSizes) + { + totalSize += size; + } + + err = MP4NewHandle(static_cast(totalSize), outSampleDataH); + if(err) return err; + + // Copy payloads directly + char *dst = reinterpret_cast(**outSampleDataH); + for(u32 n = 0; n < sortedItems.size(); ++n) + { + const MetadataItem &item = sortedItems[n]; + std::memcpy(dst, item.payload.data(), item.payload.size()); + dst += metadataSizes[n]; + } + + return MP4NoErr; +} + +bool DedicatedIt35Strategy::isApplicable(const MetadataMap &items, const InjectionConfig &config, + std::string &reason) const +{ + if(!config.movie) + { + reason = "No movie provided"; + return false; + } + + if(items.empty()) + { + reason = "No metadata items to inject"; + return false; + } + + return true; +} + +MP4Err DedicatedIt35Strategy::inject(const InjectionConfig &config, const MetadataMap &items, + const T35Prefix &prefix) +{ + LOG_INFO("Injecting metadata using dedicated IT35 track strategy"); + + MP4Err err = MP4NoErr; + MP4Track trakM = nullptr; // metadata track + MP4Track trakV = nullptr; // reference to video track + MP4Media mediaM = nullptr; + MP4Handle sampleDataH = nullptr; + MP4Handle durationsH = nullptr; + MP4Handle sizesH = nullptr; + MP4Media videoMedia = nullptr; + u32 videoTimescale = 0; + u32 sampleCount = 0; + std::vector videoDurations; + std::vector metadataDurations; + std::vector metadataSizes; + std::vector sortedItems; + + // Find video track + LOG_DEBUG("Finding first video track"); + err = findFirstVideoTrack(config.movie, &trakV); + if(err) + { + LOG_ERROR("Failed to find video track (err={})", err); + goto bail; + } + + // Get video media and timescale + err = MP4GetTrackMedia(trakV, &videoMedia); + if(err) + { + LOG_ERROR("Failed to get video media (err={})", err); + goto bail; + } + + err = MP4GetMediaTimeScale(videoMedia, &videoTimescale); + if(err) + { + videoTimescale = 1000; // default to 1000 if not available + LOG_WARN("Failed to get video timescale, defaulting to 1000"); + } + LOG_DEBUG("Video timescale: {}", videoTimescale); + + // Get video sample durations + err = getVideoSampleDurations(videoMedia, videoDurations); + if(err) + { + LOG_ERROR("Failed to get video sample durations (err={})", err); + goto bail; + } + + // Create dedicated IT35 metadata track + LOG_DEBUG("Creating dedicated IT35 metadata track with T.35 prefix: {}", prefix.toString()); + err = ISONewT35MetadataTrack(config.movie, videoTimescale, prefix.toString().c_str(), + trakV, // video track reference + MP4RndrTrackReferenceAtomType, // 'rndr' track reference + &trakM, &mediaM); + if(err) + { + LOG_ERROR("Failed to create IT35 metadata track (err={})", err); + goto bail; + } + + // Build metadata durations and sizes + err = buildMetadataDurationsAndSizes(items, videoDurations, metadataDurations, metadataSizes, + sortedItems); + if(err) + { + LOG_ERROR("Failed to build metadata durations/sizes (err={})", err); + goto bail; + } + + sampleCount = static_cast(sortedItems.size()); + LOG_DEBUG("Prepared {} metadata samples", sampleCount); + + // Build durations handle + err = MP4NewHandle(sampleCount * sizeof(u32), &durationsH); + if(err) + { + LOG_ERROR("Failed to create durations handle (err={})", err); + goto bail; + } + + { + u32 *durationPtr = reinterpret_cast(*durationsH); + for(u32 n = 0; n < sampleCount; ++n) + { + durationPtr[n] = metadataDurations[n]; + } + } + + // Build sizes handle + err = MP4NewHandle(sampleCount * sizeof(u32), &sizesH); + if(err) + { + LOG_ERROR("Failed to create sizes handle (err={})", err); + goto bail; + } + + { + u32 *sizePtr = reinterpret_cast(*sizesH); + for(u32 n = 0; n < sampleCount; ++n) + { + sizePtr[n] = metadataSizes[n]; + } + } + + // Build sample data (just payloads, no box wrapper) + err = buildSampleData(sortedItems, metadataSizes, &sampleDataH); + if(err) + { + LOG_ERROR("Failed to build sample data (err={})", err); + goto bail; + } + + // Add all samples in one call + err = MP4AddMediaSamples(mediaM, sampleDataH, sampleCount, durationsH, sizesH, + 0, // reuse sample entry + 0, // no decoding offsets + 0); // all sync samples + if(err) + { + LOG_ERROR("MP4AddMediaSamples failed (err={})", err); + goto bail; + } + + LOG_INFO("Added {} metadata samples to dedicated IT35 track", sampleCount); + + // Finalize media edits to commit durations + err = MP4EndMediaEdits(mediaM); + if(err) + { + LOG_ERROR("Failed to end media edits (err={})", err); + goto bail; + } + + LOG_INFO("Metadata injection complete"); + +bail: + if(sampleDataH) MP4DisposeHandle(sampleDataH); + if(durationsH) MP4DisposeHandle(durationsH); + if(sizesH) MP4DisposeHandle(sizesH); + + return err; +} + +} // namespace t35 diff --git a/IsoLib/t35_tool/injection/DedicatedIt35Strategy.hpp b/IsoLib/t35_tool/injection/DedicatedIt35Strategy.hpp new file mode 100644 index 00000000..d0d49ed0 --- /dev/null +++ b/IsoLib/t35_tool/injection/DedicatedIt35Strategy.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include "InjectionStrategy.hpp" + +namespace t35 { + +/** + * @brief Injection strategy using dedicated IT35 metadata track ('it35' sample entry). + * + * Creates a dedicated T.35 timed metadata track with: + * - Sample entry type: 'it35' (T35MetadataSampleEntry) + * - t35_identifier and description fields in sample entry + * - Samples contain only T.35 payload (no box wrapper) + * - Optional 'rndr' track reference to video track + * + * This is the standardized approach for dedicated T.35 metadata tracks. + */ +class DedicatedIt35Strategy : public InjectionStrategy { +public: + DedicatedIt35Strategy() = default; + ~DedicatedIt35Strategy() override = default; + + std::string getName() const override { return "dedicated-it35"; } + + std::string getDescription() const override { + return "Dedicated IT35 metadata track with description and t35_identifier"; + } + + bool isApplicable(const MetadataMap& items, + const InjectionConfig& config, + std::string& reason) const override; + + MP4Err inject(const InjectionConfig& config, + const MetadataMap& items, + const T35Prefix& prefix) override; +}; + +} // namespace t35 diff --git a/IsoLib/t35_tool/injection/InjectionStrategy.cpp b/IsoLib/t35_tool/injection/InjectionStrategy.cpp new file mode 100644 index 00000000..68436369 --- /dev/null +++ b/IsoLib/t35_tool/injection/InjectionStrategy.cpp @@ -0,0 +1,37 @@ +#include "InjectionStrategy.hpp" +#include "MebxMe4cStrategy.hpp" +#include "DedicatedIt35Strategy.hpp" +#include "SampleGroupStrategy.hpp" +#include "../common/Logger.hpp" + +namespace t35 +{ + +std::unique_ptr createInjectionStrategy(const std::string &strategyName) +{ + LOG_DEBUG("Creating injection strategy: '{}'", strategyName); + + if(strategyName == "mebx-me4c") + { + return std::make_unique(); + } + else if(strategyName == "dedicated-it35") + { + return std::make_unique(); + } + else if(strategyName == "sample-group") + { + return std::make_unique(); + } + else if(strategyName == "sei") + { + throw T35Exception(T35Error::NotImplemented, + "Injection strategy '" + strategyName + "' is not yet implemented"); + } + else + { + throw T35Exception(T35Error::InjectionFailed, "Unknown injection strategy: " + strategyName); + } +} + +} // namespace t35 diff --git a/IsoLib/t35_tool/injection/InjectionStrategy.hpp b/IsoLib/t35_tool/injection/InjectionStrategy.hpp new file mode 100644 index 00000000..4898dc50 --- /dev/null +++ b/IsoLib/t35_tool/injection/InjectionStrategy.hpp @@ -0,0 +1,90 @@ +#pragma once + +#include "../common/MetadataTypes.hpp" +#include "../common/T35Prefix.hpp" + +// Forward declarations from libisomediafile +extern "C" { +typedef int MP4Err; +} + +#include +#include + +namespace t35 { + +/** + * Abstract interface for metadata injection strategies + * + * An InjectionStrategy handles: + * - Creating MP4 container structures (tracks, sample entries, etc.) + * - Adding metadata samples to the container + * - Linking metadata to video track + * + * Different strategies implement different MP4 storage methods: + * - MEBX tracks (me4c namespace) + * - Dedicated metadata tracks + * - Sample groups + * - Sample entry boxes + */ +class InjectionStrategy { +public: + virtual ~InjectionStrategy() = default; + + /** + * Get strategy name + * @return Name string (e.g., "mebx-me4c", "dedicated-it35") + */ + virtual std::string getName() const = 0; + + /** + * Get strategy description + * @return Human-readable description + */ + virtual std::string getDescription() const = 0; + + /** + * Check if this strategy is applicable to the given metadata + * + * Some strategies have constraints: + * - Static metadata only (sample-entry-box, default-sample-group) + * - Specific codec requirements + * + * @param items Metadata to check + * @param config Injection configuration + * @param reason Output parameter for reason if not applicable + * @return true if strategy can be used + */ + virtual bool isApplicable(const MetadataMap& items, + const InjectionConfig& config, + std::string& reason) const = 0; + + /** + * Inject metadata into movie + * + * @param config Configuration (movie, video track, etc.) + * @param items Metadata to inject + * @param prefix T.35 prefix for this metadata + * @return MP4Err (0 = success) + * @throws T35Exception on error + */ + virtual MP4Err inject(const InjectionConfig& config, + const MetadataMap& items, + const T35Prefix& prefix) = 0; +}; + +/** + * Factory function to create injection strategy from name + * + * Available strategies: + * - "mebx-me4c": MEBX track with me4c namespace + * - "dedicated-it35": Dedicated metadata track + * - "sample-group": Sample group + * + * @param strategyName Strategy name + * @return Unique pointer to InjectionStrategy + * @throws T35Exception if strategy is unknown + */ +std::unique_ptr createInjectionStrategy(const std::string& strategyName); + +} // namespace t35 diff --git a/IsoLib/t35_tool/injection/MebxMe4cStrategy.cpp b/IsoLib/t35_tool/injection/MebxMe4cStrategy.cpp new file mode 100644 index 00000000..0bae4416 --- /dev/null +++ b/IsoLib/t35_tool/injection/MebxMe4cStrategy.cpp @@ -0,0 +1,557 @@ +#include "MebxMe4cStrategy.hpp" +#include "../common/Logger.hpp" + +extern "C" +{ +#include "MP4Movies.h" +#include "MP4Atoms.h" +} + +#include +#include +#include + +namespace t35 +{ + +// Helper: Find first video track +static MP4Err findFirstVideoTrack(MP4Movie moov, MP4Track *outTrack) +{ + MP4Err err = MP4NoErr; + u32 trackCount = 0; + *outTrack = nullptr; + + err = MP4GetMovieTrackCount(moov, &trackCount); + if(err) return err; + + MP4Track firstVideo = nullptr; + u32 videoCount = 0; + + for(u32 i = 1; i <= trackCount; ++i) + { + MP4Track trak = nullptr; + MP4Media media = nullptr; + u32 handlerType = 0; + + err = MP4GetMovieIndTrack(moov, i, &trak); + if(err) continue; + + err = MP4GetTrackMedia(trak, &media); + if(err) continue; + + err = MP4GetMediaHandlerDescription(media, &handlerType, nullptr); + if(err) continue; + + if(handlerType == MP4VisualHandlerType) + { + if(!firstVideo) + { + firstVideo = trak; + } + ++videoCount; + } + } + + if(!firstVideo) + { + LOG_ERROR("No video track found in movie"); + return MP4NotFoundErr; + } + + if(videoCount > 1) + { + LOG_WARN("Found {} video tracks, using the first one", videoCount); + } + + *outTrack = firstVideo; + return MP4NoErr; +} + +// Helper: Get video sample durations +static MP4Err getVideoSampleDurations(MP4Media mediaV, std::vector &durations) +{ + MP4Err err = MP4NoErr; + u32 sampleCount = 0; + + durations.clear(); + + err = MP4GetMediaSampleCount(mediaV, &sampleCount); + if(err) return err; + + durations.reserve(sampleCount); + + for(u32 i = 1; i <= sampleCount; ++i) + { + MP4Handle sampleH = nullptr; + u32 outSize, outSampleFlags, outSampleDescIndex; + u64 outDTS, outDuration; + s32 outCTSOffset; + + MP4NewHandle(0, &sampleH); + err = MP4GetIndMediaSample(mediaV, i, sampleH, &outSize, &outDTS, &outCTSOffset, &outDuration, + &outSampleFlags, &outSampleDescIndex); + if(err) + { + if(sampleH) MP4DisposeHandle(sampleH); + return err; + } + + durations.push_back(static_cast(outDuration)); + + if(sampleH) MP4DisposeHandle(sampleH); + } + + LOG_DEBUG("Collected {} video sample durations", durations.size()); + return MP4NoErr; +} + +// Helper: Build metadata durations and sizes +static MP4Err buildMetadataDurationsAndSizes(const MetadataMap &items, + const std::vector &videoDurations, + std::vector &metadataDurations, + std::vector &metadataSizes, + std::vector &sortedItems) +{ + metadataDurations.clear(); + metadataSizes.clear(); + sortedItems.clear(); + + if(items.empty()) + { + LOG_ERROR("No metadata items provided"); + return MP4BadParamErr; + } + + // Sort items by frame_start (already sorted in map, but make vector) + for(const auto &[start, item] : items) + { + sortedItems.push_back(item); + } + + // Validate coverage + const auto &last = sortedItems.back(); + u32 maxFrame = last.frame_start + last.frame_duration; + if(maxFrame > videoDurations.size()) + { + LOG_ERROR("Metadata covers up to frame {} but video only has {} samples", maxFrame, + videoDurations.size()); + return MP4BadParamErr; + } + + // Compute metadata sample durations and sizes + for(const auto &item : sortedItems) + { + u32 startFrame = item.frame_start; + u32 endFrame = startFrame + item.frame_duration; + u32 totalDur = 0; + + for(u32 f = startFrame; f < endFrame; ++f) + { + totalDur += videoDurations[f]; + } + + metadataDurations.push_back(totalDur); + metadataSizes.push_back(static_cast(item.payload.size())); + + LOG_DEBUG("Metadata item covers frames [{}-{}] totalDur={} size={} bytes", startFrame, + endFrame - 1, totalDur, item.payload.size()); + } + + return MP4NoErr; +} + +// Helper: Add all metadata samples +static MP4Err addAllMetadataSamples(MP4Media mediaM, const std::vector &sortedItems, + const std::vector &metadataDurations, + const std::vector &metadataSizes, u32 local_key_id) +{ + MP4Err err = MP4NoErr; + u32 sampleCount = static_cast(sortedItems.size()); + + MP4Handle durationsH = nullptr; + MP4Handle sizesH = nullptr; + MP4Handle sampleDataH = nullptr; + u64 totalSize = 0; + + if(sampleCount == 0) + { + LOG_ERROR("No metadata samples to add"); + return MP4BadParamErr; + } + + // --- Durations handle --- + { + bool allSame = std::all_of(metadataDurations.begin(), metadataDurations.end(), + [&](u32 d) { return d == metadataDurations[0]; }); + if(allSame) + { + err = MP4NewHandle(sizeof(u32), &durationsH); + if(err) goto bail; + *((u32 *)*durationsH) = metadataDurations[0]; + } + else + { + err = MP4NewHandle(sizeof(u32) * sampleCount, &durationsH); + if(err) goto bail; + for(u32 n = 0; n < sampleCount; ++n) + { + ((u32 *)*durationsH)[n] = metadataDurations[n]; + } + } + } + + // --- Sizes handle --- + { + bool allSame = std::all_of(metadataSizes.begin(), metadataSizes.end(), + [&](u32 s) { return s == metadataSizes[0]; }); + if(allSame) + { + err = MP4NewHandle(sizeof(u32), &sizesH); + if(err) goto bail; + *((u32 *)*sizesH) = metadataSizes[0] + 8; // +4 box_size +4 box_type + } + else + { + err = MP4NewHandle(sizeof(u32) * sampleCount, &sizesH); + if(err) goto bail; + for(u32 n = 0; n < sampleCount; ++n) + { + ((u32 *)*sizesH)[n] = metadataSizes[n] + 8; // +4 box_size +4 box_type + } + } + } + + // --- Sample data handle --- + totalSize = 0; + for(u32 n = 0; n < sampleCount; ++n) + { + totalSize += metadataSizes[n] + 8; + } + err = MP4NewHandle((u32)totalSize, &sampleDataH); + if(err) goto bail; + + { + char *dst = reinterpret_cast(*sampleDataH); + for(u32 n = 0; n < sampleCount; ++n) + { + const MetadataItem &item = sortedItems[n]; + + u32 boxSize = 8 + metadataSizes[n]; + // write size + dst[0] = (boxSize >> 24) & 0xFF; + dst[1] = (boxSize >> 16) & 0xFF; + dst[2] = (boxSize >> 8) & 0xFF; + dst[3] = (boxSize) & 0xFF; + // write type (local_key_id) + dst[4] = (local_key_id >> 24) & 0xFF; + dst[5] = (local_key_id >> 16) & 0xFF; + dst[6] = (local_key_id >> 8) & 0xFF; + dst[7] = (local_key_id) & 0xFF; + dst += 8; + + // Copy payload from memory + std::memcpy(dst, item.payload.data(), item.payload.size()); + dst += metadataSizes[n]; + } + } + + // --- Add all samples in one call --- + err = MP4AddMediaSamples(mediaM, sampleDataH, sampleCount, durationsH, sizesH, + 0, // reuse sample entry + 0, // no decoding offsets + 0); // all sync samples + if(err) + { + LOG_ERROR("MP4AddMediaSamples failed (err={})", err); + goto bail; + } + + LOG_INFO("Added {} metadata samples", sampleCount); + +bail: + if(sampleDataH) MP4DisposeHandle(sampleDataH); + if(durationsH) MP4DisposeHandle(durationsH); + if(sizesH) MP4DisposeHandle(sizesH); + + return err; +} + +// Helper: Create handle with 4-character code +static MP4Err fourCCToHandle(u32 fourCC, MP4Handle *outHandle) +{ + MP4Err err = MP4NoErr; + *outHandle = nullptr; + + err = MP4NewHandle(4, outHandle); + if(err) return err; + + char *data = (char *)**outHandle; // Dereference to get the data pointer + data[0] = (fourCC >> 24) & 0xFF; + data[1] = (fourCC >> 16) & 0xFF; + data[2] = (fourCC >> 8) & 0xFF; + data[3] = (fourCC) & 0xFF; + + return MP4NoErr; +} + +// Helper: Convert string to handle (as text, not hex) +static MP4Err stringToHandle(const std::string &input, MP4Handle *outHandle) +{ + MP4Err err = MP4NoErr; + *outHandle = nullptr; + + // Copy input string as text + u32 byteCount = static_cast(input.size()); + err = MP4NewHandle(byteCount, outHandle); + if(err) return err; + + std::memcpy(**outHandle, input.data(), byteCount); + + return MP4NoErr; +} + +bool MebxMe4cStrategy::isApplicable(const MetadataMap &items, const InjectionConfig &config, + std::string &reason) const +{ + if(!config.movie) + { + reason = "No movie provided"; + return false; + } + + if(items.empty()) + { + reason = "No metadata items to inject"; + return false; + } + + return true; +} + +MP4Err MebxMe4cStrategy::inject(const InjectionConfig &config, const MetadataMap &items, + const T35Prefix &prefix) +{ + LOG_INFO("Injecting metadata using mebx-me4c strategy"); + + MP4Err err = MP4NoErr; + MP4Track trakM = nullptr; // metadata track + MP4Track trakV = nullptr; // reference to video track + MP4Media mediaM = nullptr; + + // Find video track + LOG_DEBUG("Finding first video track"); + err = findFirstVideoTrack(config.movie, &trakV); + if(err) + { + LOG_ERROR("Failed to find video track (err={})", err); + return err; + } + + // Create mebx track + LOG_DEBUG("Creating mebx track"); + err = MP4NewMovieTrack(config.movie, MP4NewTrackIsMebx, &trakM); + if(err) + { + LOG_ERROR("Failed to create mebx track (err={})", err); + return err; + } + + // Get video media and timescale + MP4Media videoMedia = nullptr; + u32 videoTimescale = 0; + err = MP4GetTrackMedia(trakV, &videoMedia); + if(err) + { + LOG_ERROR("Failed to get video media (err={})", err); + return err; + } + + err = MP4GetMediaTimeScale(videoMedia, &videoTimescale); + if(err) + { + videoTimescale = 1000; // default to 1000 if not available + LOG_WARN("Failed to get video timescale, defaulting to 1000"); + } + LOG_DEBUG("Video timescale: {}", videoTimescale); + + // Get video sample durations + std::vector videoDurations; + err = getVideoSampleDurations(videoMedia, videoDurations); + if(err) + { + LOG_ERROR("Failed to get video sample durations (err={})", err); + return err; + } + + // Create mebx media with same timescale as video + LOG_DEBUG("Creating mebx media with timescale {}", videoTimescale); + err = MP4NewTrackMedia(trakM, &mediaM, MP4MetaHandlerType, videoTimescale, NULL); + if(err) + { + LOG_ERROR("Failed to create mebx media (err={})", err); + return err; + } + + // Link metadata track to video track using 'rndr' track reference + LOG_DEBUG("Adding track reference"); + err = MP4AddTrackReference(trakM, trakV, MP4_FOUR_CHAR_CODE('r', 'n', 'd', 'r'), 0); + if(err) + { + LOG_ERROR("Failed to add track reference (err={})", err); + return err; + } + + // Create mebx sample entry + LOG_DEBUG("Creating mebx sample entry"); + MP4BoxedMetadataSampleEntryPtr mebx = nullptr; + err = ISONewMebxSampleDescription(&mebx, 1); + if(err) + { + LOG_ERROR("Failed to create mebx sample description (err={})", err); + return err; + } + + // For me4c strategy: + // - key_namespace = 'me4c' + // - key_value = 'it35' (4-character code) + // - setupInfo = T.35 prefix string + LOG_DEBUG("Using me4c namespace with it35 key_value"); + + // Build key_value as 'it35' 4CC + MP4Handle key_value = nullptr; + err = fourCCToHandle(MP4_FOUR_CHAR_CODE('i', 't', '3', '5'), &key_value); + if(err) + { + LOG_ERROR("Failed to create it35 key_value handle (err={})", err); + return err; + } + LOG_DEBUG("Created key_value handle with it35 4CC"); + + // Build setupInfo with T.35 prefix in binary format: + // 1. utf8string description (null-terminated, '\0' if empty) + // 2. unsigned int(8) t35_identifier[] (binary bytes) + MP4Handle setupInfo = nullptr; + { + const std::string &desc = prefix.description(); + std::vector identifierBytes = prefix.toBytes(); + + // Calculate total size: description length + null terminator + identifier bytes + u32 descLen = desc.empty() ? 1 : (u32)desc.size() + 1; // '\0' if empty, or string + '\0' + u32 totalSize = descLen + (u32)identifierBytes.size(); + + err = MP4NewHandle(totalSize, &setupInfo); + if(err) + { + LOG_ERROR("Failed to create setupInfo handle (err={})", err); + MP4DisposeHandle(key_value); + return err; + } + + char *buffer = *setupInfo; + u32 offset = 0; + + // Write description as null-terminated UTF-8 string + if(desc.empty()) + { + buffer[offset++] = '\0'; // Just null byte if no description + } + else + { + memcpy(buffer + offset, desc.c_str(), desc.size()); + offset += desc.size(); + buffer[offset++] = '\0'; // Null terminator + } + + // Write t35_identifier as binary bytes + if(!identifierBytes.empty()) + { + memcpy(buffer + offset, identifierBytes.data(), identifierBytes.size()); + offset += identifierBytes.size(); + } + + LOG_DEBUG("Created setupInfo handle: description='{}' ({} bytes), identifier={} bytes", + desc.empty() ? "(empty)" : desc, descLen, identifierBytes.size()); + } + + // Add sample entry with me4c namespace + // For me4c namespace, desired_local_key_id must match the 4CC in key_value + u32 desired_key_id = MP4_FOUR_CHAR_CODE('i', 't', '3', '5'); + u32 local_key_id = 0; + LOG_DEBUG("Calling ISOAddMebxMetadataToSampleEntry with me4c namespace"); + err = ISOAddMebxMetadataToSampleEntry(mebx, + desired_key_id, // Must match key_value for me4c + &local_key_id, + MP4_FOUR_CHAR_CODE('m', 'e', '4', 'c'), // me4c namespace + key_value, // 'it35' 4CC + NULL, // locale_string (not used) + setupInfo); // T.35 prefix string + + MP4DisposeHandle(key_value); + MP4DisposeHandle(setupInfo); + + if(err) + { + LOG_ERROR("Failed to add mebx metadata to sample entry (err={})", err); + return err; + } + + MP4Handle sampleEntryMH = nullptr; + err = MP4NewHandle(0, &sampleEntryMH); + if(err) + { + LOG_ERROR("Failed to create sample entry handle (err={})", err); + return err; + } + + err = ISOGetMebxHandle(mebx, sampleEntryMH); + if(err) + { + LOG_ERROR("Failed to get mebx handle (err={})", err); + return err; + } + + err = MP4AddMediaSamples(mediaM, 0, 0, 0, 0, sampleEntryMH, 0, 0); + if(err) + { + LOG_ERROR("Failed to add sample entry (err={})", err); + return err; + } + + LOG_INFO("MEBX track and sample entry created successfully"); + LOG_INFO("Local key ID = {}", local_key_id); + LOG_INFO("Namespace: me4c, Key: it35, Setup: {}", prefix.toString()); + + // Prepare metadata sample durations and sizes + std::vector metadataDurations; + std::vector metadataSizes; + std::vector sortedItems; + + err = buildMetadataDurationsAndSizes(items, videoDurations, metadataDurations, metadataSizes, + sortedItems); + if(err) + { + LOG_ERROR("Failed to build metadata durations and sizes (err={})", err); + return err; + } + + // Add all metadata samples + err = addAllMetadataSamples(mediaM, sortedItems, metadataDurations, metadataSizes, local_key_id); + if(err) + { + LOG_ERROR("Failed to add metadata samples (err={})", err); + return err; + } + + // End media edits + err = MP4EndMediaEdits(mediaM); + if(err) + { + LOG_ERROR("Failed to end media edits (err={})", err); + return err; + } + + LOG_INFO("Metadata injection complete"); + return MP4NoErr; +} + +} // namespace t35 diff --git a/IsoLib/t35_tool/injection/MebxMe4cStrategy.hpp b/IsoLib/t35_tool/injection/MebxMe4cStrategy.hpp new file mode 100644 index 00000000..effeb664 --- /dev/null +++ b/IsoLib/t35_tool/injection/MebxMe4cStrategy.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "InjectionStrategy.hpp" + +namespace t35 { + +/** + * MEBX track with me4c namespace injection strategy + * Future implementation + */ +class MebxMe4cStrategy : public InjectionStrategy { +public: + MebxMe4cStrategy() = default; + virtual ~MebxMe4cStrategy() = default; + + std::string getName() const override { return "mebx-me4c"; } + + std::string getDescription() const override { + return "MEBX metadata track with me4c namespace"; + } + + bool isApplicable(const MetadataMap& items, + const InjectionConfig& config, + std::string& reason) const override; + + MP4Err inject(const InjectionConfig& config, + const MetadataMap& items, + const T35Prefix& prefix) override; +}; + +} // namespace t35 diff --git a/IsoLib/t35_tool/injection/SampleGroupStrategy.cpp b/IsoLib/t35_tool/injection/SampleGroupStrategy.cpp new file mode 100644 index 00000000..79679cac --- /dev/null +++ b/IsoLib/t35_tool/injection/SampleGroupStrategy.cpp @@ -0,0 +1,210 @@ +#include "SampleGroupStrategy.hpp" +#include "../common/Logger.hpp" +#include "../common/T35Prefix.hpp" +#include "../common/MetadataTypes.hpp" + +extern "C" +{ +#include "MP4Movies.h" +#include "MP4Atoms.h" +} + +#include +#include + +namespace t35 +{ + +// Helper: Find first video track +static MP4Err findFirstVideoTrack(MP4Movie moov, MP4Track *outTrack) +{ + MP4Err err = MP4NoErr; + u32 trackCount = 0; + *outTrack = nullptr; + + err = MP4GetMovieTrackCount(moov, &trackCount); + if(err) return err; + + for(u32 i = 1; i <= trackCount; ++i) + { + MP4Track trak = nullptr; + MP4Media media = nullptr; + u32 handlerType = 0; + + err = MP4GetMovieIndTrack(moov, i, &trak); + if(err) continue; + + err = MP4GetTrackMedia(trak, &media); + if(err) continue; + + err = MP4GetMediaHandlerDescription(media, &handlerType, nullptr); + if(err) continue; + + if(handlerType == MP4VisualHandlerType) + { + *outTrack = trak; + return MP4NoErr; + } + } + + return MP4NotFoundErr; +} + +bool SampleGroupStrategy::isApplicable(const MetadataMap &items, const InjectionConfig &config, + std::string &reason) const +{ + (void)config; // Unused for this strategy + + if(items.empty()) + { + reason = "No metadata items to inject"; + return false; + } + + // Sample groups can handle both static and dynamic metadata + return true; +} + +MP4Err SampleGroupStrategy::inject(const InjectionConfig &config, const MetadataMap &items, + const T35Prefix &prefix) +{ + LOG_INFO("Injecting metadata using sample-group strategy"); + LOG_INFO("T.35 prefix: {}", prefix.toString()); + + MP4Err err = MP4NoErr; + + // Validate + if(!config.movie) + { + LOG_ERROR("Invalid injection config: missing movie"); + return MP4BadParamErr; + } + + if(items.empty()) + { + LOG_ERROR("No metadata items to inject"); + return MP4BadParamErr; + } + + // Find video track + LOG_DEBUG("Finding first video track"); + MP4Track videoTrack = nullptr; + err = findFirstVideoTrack(config.movie, &videoTrack); + if(err) + { + LOG_ERROR("Failed to find video track (err={})", err); + return err; + } + + // Get video media + MP4Media videoMedia = nullptr; + err = MP4GetTrackMedia(videoTrack, &videoMedia); + if(err != MP4NoErr) + { + LOG_ERROR("Failed to get video track media (err={})", err); + return err; + } + + u32 videoTrackID = 0; + MP4GetTrackID(videoTrack, &videoTrackID); + LOG_INFO("Using video track ID {}", videoTrackID); + + // Get video sample count + u32 videoSampleCount = 0; + err = MP4GetMediaSampleCount(videoMedia, &videoSampleCount); + if(err != MP4NoErr) + { + LOG_ERROR("Failed to get video sample count (err={})", err); + return err; + } + LOG_INFO("Video track has {} samples", videoSampleCount); + + // Sort metadata items by frame_start + std::vector> sortedItems(items.begin(), items.end()); + std::sort(sortedItems.begin(), sortedItems.end(), + [](const auto &a, const auto &b) { return a.first < b.first; }); + + LOG_INFO("Processing {} metadata items", sortedItems.size()); + + // For each metadata item: + // 1. Add T.35 group description (sgpd) + // 2. Map video samples to this group (sbgp) + + for(size_t i = 0; i < sortedItems.size(); ++i) + { + const auto &[frameStart, item] = sortedItems[i]; + u32 frameEnd = frameStart + item.frame_duration; + + LOG_DEBUG("Processing metadata item {}: frames {}-{} ({} frames)", i + 1, frameStart, + frameEnd - 1, item.frame_duration); + + // Create handle for T.35 payload + MP4Handle t35DataH = nullptr; + err = MP4NewHandle(item.payload.size(), &t35DataH); + if(err != MP4NoErr) + { + LOG_ERROR("Failed to create handle for T.35 data (err={})", err); + return err; + } + + std::memcpy(*t35DataH, item.payload.data(), item.payload.size()); + + // Add T.35 group description + u32 groupIndex = 0; + err = ISOAddT35GroupDescription(videoMedia, t35DataH, + 1, // complete_message_flag = 1 + &groupIndex); + + MP4DisposeHandle(t35DataH); + + if(err != MP4NoErr) + { + LOG_ERROR("Failed to add T.35 group description (err={})", err); + return err; + } + + LOG_DEBUG("Added T.35 group description at index {}", groupIndex); + + // Calculate which video samples correspond to these frames + // video sample index is 1-based + u32 firstSample = frameStart + 1; // 1-based + u32 sampleCount = item.frame_duration; + + // Clamp to video track bounds + if(firstSample > videoSampleCount) + { + LOG_WARN("Metadata starts at frame {} but video only has {} samples, skipping", frameStart, + videoSampleCount); + continue; + } + + if(firstSample + sampleCount - 1 > videoSampleCount) + { + u32 oldCount = sampleCount; + sampleCount = videoSampleCount - firstSample + 1; + LOG_WARN("Metadata extends beyond video track: clamping from {} to {} samples", oldCount, + sampleCount); + } + + // Map video samples to this group + err = ISOMapSamplestoGroup(videoMedia, + MP4T35SampleGroupEntry, // 'it35' + groupIndex, firstSample, sampleCount); + + if(err != MP4NoErr) + { + LOG_ERROR("Failed to map samples to group (err={})", err); + return err; + } + + LOG_INFO("Mapped video samples {}-{} to T.35 group {} ({} bytes)", firstSample, + firstSample + sampleCount - 1, groupIndex, item.payload.size()); + } + + LOG_INFO("Successfully injected {} T.35 metadata items into video track using sample groups", + sortedItems.size()); + + return MP4NoErr; +} + +} // namespace t35 diff --git a/IsoLib/t35_tool/injection/SampleGroupStrategy.hpp b/IsoLib/t35_tool/injection/SampleGroupStrategy.hpp new file mode 100644 index 00000000..a1edab68 --- /dev/null +++ b/IsoLib/t35_tool/injection/SampleGroupStrategy.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include "InjectionStrategy.hpp" + +namespace t35 { + +/** + * Sample Group injection strategy + * Injects T.35 metadata into video track using sample groups (sgpd/sbgp) + * + * This strategy modifies the video track by: + * - Adding sample group descriptions (sgpd) with 'it35' grouping type + * - Adding sample-to-group mappings (sbgp) to associate samples with metadata + * + * Supports both static (all samples → one group) and dynamic (different samples → different groups) metadata. + */ +class SampleGroupStrategy : public InjectionStrategy { +public: + SampleGroupStrategy() = default; + virtual ~SampleGroupStrategy() = default; + + std::string getName() const override { return "sample-group"; } + + std::string getDescription() const override { + return "Inject T.35 metadata into video track using sample groups (sgpd/sbgp)"; + } + + bool isApplicable(const MetadataMap& items, + const InjectionConfig& config, + std::string& reason) const override; + + MP4Err inject(const InjectionConfig& config, + const MetadataMap& items, + const T35Prefix& prefix) override; +}; + +} // namespace t35 diff --git a/IsoLib/t35_tool/sources/GenericJsonSource.cpp b/IsoLib/t35_tool/sources/GenericJsonSource.cpp new file mode 100644 index 00000000..b6408654 --- /dev/null +++ b/IsoLib/t35_tool/sources/GenericJsonSource.cpp @@ -0,0 +1,220 @@ +#include "GenericJsonSource.hpp" +#include "../common/Logger.hpp" + +#include +#include +#include + +namespace t35 +{ + +GenericJsonSource::GenericJsonSource(const std::string &jsonPath) + : path(jsonPath) +{ +} + +bool GenericJsonSource::validate(std::string &errorMsg) +{ + LOG_DEBUG("Validating GenericJsonSource at {}", path); + + // Check if file exists + if(!std::filesystem::exists(path)) + { + errorMsg = "JSON file does not exist: " + path; + return false; + } + + // Check if it's a regular file + if(!std::filesystem::is_regular_file(path)) + { + errorMsg = "Path is not a regular file: " + path; + return false; + } + + // Try to parse JSON + try + { + std::ifstream file(path); + if(!file.is_open()) + { + errorMsg = "Cannot open JSON file: " + path; + return false; + } + + nlohmann::json j; + file >> j; + + // Validate JSON schema + if(!j.contains("items")) + { + errorMsg = "JSON missing required field: 'items'"; + return false; + } + + if(!j["items"].is_array()) + { + errorMsg = "JSON field 'items' must be an array"; + return false; + } + + // Get base directory for relative paths + std::filesystem::path jsonDir = std::filesystem::path(path).parent_path(); + if(jsonDir.empty()) + { + jsonDir = "."; + } + + // Validate each item + const auto &items = j["items"]; + for(size_t i = 0; i < items.size(); ++i) + { + const auto &item = items[i]; + + // Check required fields + if(!item.contains("frame_start")) + { + errorMsg = "Item " + std::to_string(i) + " missing 'frame_start'"; + return false; + } + if(!item.contains("frame_duration")) + { + errorMsg = "Item " + std::to_string(i) + " missing 'frame_duration'"; + return false; + } + if(!item.contains("binary_file")) + { + errorMsg = "Item " + std::to_string(i) + " missing 'binary_file'"; + return false; + } + + // Check types + if(!item["frame_start"].is_number_unsigned()) + { + errorMsg = "Item " + std::to_string(i) + " 'frame_start' must be unsigned integer"; + return false; + } + if(!item["frame_duration"].is_number_unsigned()) + { + errorMsg = "Item " + std::to_string(i) + " 'frame_duration' must be unsigned integer"; + return false; + } + if(!item["binary_file"].is_string()) + { + errorMsg = "Item " + std::to_string(i) + " 'binary_file' must be string"; + return false; + } + + // Check if binary file exists + std::string binaryFile = item["binary_file"].get(); + std::filesystem::path binaryPath = jsonDir / binaryFile; + + if(!std::filesystem::exists(binaryPath)) + { + errorMsg = "Binary file does not exist: " + binaryPath.string(); + return false; + } + + if(!std::filesystem::is_regular_file(binaryPath)) + { + errorMsg = "Binary path is not a regular file: " + binaryPath.string(); + return false; + } + } + + LOG_DEBUG("GenericJsonSource validation passed"); + return true; + } + catch(const nlohmann::json::exception &e) + { + errorMsg = "JSON parse error: " + std::string(e.what()); + return false; + } + catch(const std::exception &e) + { + errorMsg = "Validation error: " + std::string(e.what()); + return false; + } +} + +MetadataMap GenericJsonSource::load(const T35Prefix & /* prefix */) +{ + LOG_INFO("Loading metadata from GenericJsonSource: {}", path); + + // Validate first + std::string errorMsg; + if(!validate(errorMsg)) + { + LOG_ERROR("Validation failed: {}", errorMsg); + throw T35Exception(T35Error::InvalidJSON, errorMsg); + } + + // Parse JSON + std::ifstream file(path); + nlohmann::json j; + file >> j; + + // Get base directory for relative paths + std::filesystem::path jsonDir = std::filesystem::path(path).parent_path(); + if(jsonDir.empty()) + { + jsonDir = "."; + } + + MetadataMap metadataMap; + + // Load each item + const auto &items = j["items"]; + for(size_t i = 0; i < items.size(); ++i) + { + const auto &item = items[i]; + + uint32_t frameStart = item["frame_start"].get(); + uint32_t frameDuration = item["frame_duration"].get(); + std::string binaryFile = item["binary_file"].get(); + + // Read binary file + std::filesystem::path binaryPath = jsonDir / binaryFile; + std::ifstream binFile(binaryPath, std::ios::binary); + if(!binFile) + { + throw T35Exception(T35Error::MissingFiles, + "Failed to open binary file: " + binaryPath.string()); + } + + // Read entire file into vector + std::vector payload((std::istreambuf_iterator(binFile)), + std::istreambuf_iterator()); + + if(payload.empty()) + { + LOG_WARN("Binary file is empty: {}", binaryPath.string()); + } + + LOG_DEBUG("Loaded item {}: frame_start={}, frame_duration={}, payload_size={}", i, frameStart, + frameDuration, payload.size()); + + // Create metadata item + MetadataItem metaItem(frameStart, frameDuration, std::move(payload), binaryPath.string()); + + // Insert into map (key = frame_start) + if(metadataMap.find(frameStart) != metadataMap.end()) + { + throw T35Exception(T35Error::InvalidJSON, + "Duplicate frame_start: " + std::to_string(frameStart)); + } + + metadataMap[frameStart] = std::move(metaItem); + } + + LOG_INFO("Loaded {} metadata items from {}", metadataMap.size(), path); + + // Validate the loaded metadata map + if(!validateMetadataMap(metadataMap, errorMsg)) + { + throw T35Exception(T35Error::ValidationFailed, errorMsg); + } + + return metadataMap; +} + +} // namespace t35 diff --git a/IsoLib/t35_tool/sources/GenericJsonSource.hpp b/IsoLib/t35_tool/sources/GenericJsonSource.hpp new file mode 100644 index 00000000..e772996c --- /dev/null +++ b/IsoLib/t35_tool/sources/GenericJsonSource.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include "MetadataSource.hpp" + +namespace t35 { + +/** + * Generic JSON source - reads simple manifest with binary file references + * + * Expected JSON format: + * { + * "t35_prefix": "B500900001", + * "items": [ + * {"frame_start": 0, "frame_duration": 24, "binary_file": "meta_001.bin"}, + * {"frame_start": 24, "frame_duration": 24, "binary_file": "meta_002.bin"} + * ] + * } + */ +class GenericJsonSource : public MetadataSource { +public: + explicit GenericJsonSource(const std::string& jsonPath); + virtual ~GenericJsonSource() = default; + + std::string getType() const override { return "generic-json"; } + MetadataMap load(const T35Prefix& prefix) override; + bool validate(std::string& errorMsg) override; + std::string getPath() const override { return path; } + +private: + std::string path; +}; + +} // namespace t35 diff --git a/IsoLib/t35_tool/sources/MetadataSource.cpp b/IsoLib/t35_tool/sources/MetadataSource.cpp new file mode 100644 index 00000000..af4cc6b1 --- /dev/null +++ b/IsoLib/t35_tool/sources/MetadataSource.cpp @@ -0,0 +1,43 @@ +#include "MetadataSource.hpp" +#include "GenericJsonSource.hpp" +#include "SMPTEFolderSource.hpp" +#include "../common/Logger.hpp" + +namespace t35 +{ + +std::unique_ptr createMetadataSource(const std::string &sourceSpec) +{ + // Parse "type:path" + size_t colonPos = sourceSpec.find(':'); + if(colonPos == std::string::npos) + { + throw T35Exception(T35Error::SourceError, + "Invalid source spec format. Expected 'type:path', got: " + sourceSpec); + } + + std::string type = sourceSpec.substr(0, colonPos); + std::string path = sourceSpec.substr(colonPos + 1); + + if(path.empty()) + { + throw T35Exception(T35Error::SourceError, "Empty path in source spec: " + sourceSpec); + } + + LOG_DEBUG("Creating source: type='{}' path='{}'", type, path); + + if(type == "generic-json" || type == "json-manifest") + { + return std::make_unique(path); + } + else if(type == "smpte-folder" || type == "json-folder") + { + return std::make_unique(path); + } + else + { + throw T35Exception(T35Error::SourceError, "Unknown source type: " + type); + } +} + +} // namespace t35 diff --git a/IsoLib/t35_tool/sources/MetadataSource.hpp b/IsoLib/t35_tool/sources/MetadataSource.hpp new file mode 100644 index 00000000..38b96fea --- /dev/null +++ b/IsoLib/t35_tool/sources/MetadataSource.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include "../common/MetadataTypes.hpp" +#include "../common/T35Prefix.hpp" +#include +#include + +namespace t35 { + +/** + * Abstract interface for metadata sources + * + * A MetadataSource handles: + * - Discovery of input metadata files + * - Parsing of source format (JSON, binary, etc.) + * - Conversion to binary T.35 payloads + * - Creation of MetadataMap with timing information + * + * Each source type knows its input format and handles conversion internally. + */ +class MetadataSource { +public: + virtual ~MetadataSource() = default; + + /** + * Get source type identifier + * @return Type string (e.g., "generic-json", "smpte-folder") + */ + virtual std::string getType() const = 0; + + /** + * Load all metadata items from source + * + * @param prefix T.35 prefix for this metadata (may be used for validation) + * @return MetadataMap with binary payloads ready for injection + * @throws T35Exception on error + */ + virtual MetadataMap load(const T35Prefix& prefix) = 0; + + /** + * Validate source before loading + * + * @param errorMsg Output parameter for error message + * @return true if source is valid and can be loaded + */ + virtual bool validate(std::string& errorMsg) = 0; + + /** + * Get source path/location + * @return Path string for debugging + */ + virtual std::string getPath() const = 0; +}; + +/** + * Factory function to create metadata source from type:path string + * + * Format: "type:path" + * Examples: + * - "generic-json:metadata.json" + * - "smpte-folder:metadata_dir" + * + * @param sourceSpec Source specification string + * @return Unique pointer to MetadataSource + * @throws T35Exception if type is unknown or path is invalid + */ +std::unique_ptr createMetadataSource(const std::string& sourceSpec); + +} // namespace t35 diff --git a/IsoLib/t35_tool/sources/SMPTEFolderSource.cpp b/IsoLib/t35_tool/sources/SMPTEFolderSource.cpp new file mode 100644 index 00000000..9c0d3e7f --- /dev/null +++ b/IsoLib/t35_tool/sources/SMPTEFolderSource.cpp @@ -0,0 +1,199 @@ +#include "SMPTEFolderSource.hpp" +#include "../common/Logger.hpp" +#include "SMPTE_ST2094_50.hpp" + +#include +#include +#include +#include + +namespace t35 +{ + +SMPTEFolderSource::SMPTEFolderSource(const std::string &folderPath) + : path(folderPath) +{ +} + +bool SMPTEFolderSource::validate(std::string &errorMsg) +{ + LOG_DEBUG("Validating SMPTEFolderSource at {}", path); + + // Check if path exists + if(!std::filesystem::exists(path)) + { + errorMsg = "Folder does not exist: " + path; + return false; + } + + // Check if it's a directory + if(!std::filesystem::is_directory(path)) + { + errorMsg = "Path is not a directory: " + path; + return false; + } + + // Check if there are any .json files + bool hasJsonFiles = false; + try + { + for(const auto &entry : std::filesystem::directory_iterator(path)) + { + if(entry.is_regular_file() && entry.path().extension() == ".json") + { + hasJsonFiles = true; + break; + } + } + } + catch(const std::filesystem::filesystem_error &e) + { + errorMsg = "Failed to read directory: " + std::string(e.what()); + return false; + } + + if(!hasJsonFiles) + { + errorMsg = "No .json files found in directory: " + path; + return false; + } + + LOG_DEBUG("SMPTEFolderSource validation passed"); + return true; +} + +MetadataMap SMPTEFolderSource::load(const T35Prefix & /* prefix */) +{ + LOG_INFO("Loading SMPTE ST2094-50 metadata from folder: {}", path); + + // Validate first + std::string errorMsg; + if(!validate(errorMsg)) + { + LOG_ERROR("Validation failed: {}", errorMsg); + throw T35Exception(T35Error::InvalidJSON, errorMsg); + } + + MetadataMap metadataMap; + + // Collect all .json files + std::vector jsonFiles; + for(const auto &entry : std::filesystem::directory_iterator(path)) + { + if(entry.is_regular_file() && entry.path().extension() == ".json") + { + jsonFiles.push_back(entry.path()); + } + } + + // Sort files by name for consistent ordering + std::sort(jsonFiles.begin(), jsonFiles.end()); + + LOG_INFO("Found {} JSON files in folder", jsonFiles.size()); + + // Process each JSON file + for(size_t i = 0; i < jsonFiles.size(); ++i) + { + const auto &jsonFile = jsonFiles[i]; + LOG_DEBUG("Processing file {}/{}: {}", i + 1, jsonFiles.size(), jsonFile.filename().string()); + + try + { + // Read JSON file + std::ifstream file(jsonFile); + if(!file.is_open()) + { + LOG_WARN("Failed to open file: {}, skipping", jsonFile.string()); + continue; + } + + nlohmann::json j; + try + { + file >> j; + } + catch(const nlohmann::json::exception &e) + { + LOG_WARN("JSON parse error in {}: {}, skipping", jsonFile.filename().string(), e.what()); + continue; + } + + // Create SMPTE encoder instance + SMPTE_ST2094_50 smpteEncoder; + + // Decode JSON to metadata items + bool error = smpteEncoder.decodeJsonToMetadataItems(j); + if(error) + { + LOG_WARN("SMPTE decoding failed for {}, skipping", jsonFile.filename().string()); + continue; + } + + // Convert metadata items to syntax elements + smpteEncoder.convertMetadataItemsToSyntaxElements(); + + // Write syntax elements to binary data + smpteEncoder.writeSyntaxElementsToBinaryData(); + + // Get the binary payload + std::vector payload = smpteEncoder.getPayloadData(); + + // Get timing info + uint32_t frameStart = smpteEncoder.getTimeIntervalStart(); + uint32_t frameDuration = smpteEncoder.getTimeintervalDuration(); + + // Validate payload + if(payload.empty()) + { + LOG_WARN("Empty payload for {}, skipping", jsonFile.filename().string()); + continue; + } + + // Check for duplicate frame_start + if(metadataMap.find(frameStart) != metadataMap.end()) + { + LOG_ERROR("Duplicate frame_start: {} in file {}", frameStart, jsonFile.filename().string()); + throw T35Exception(T35Error::InvalidJSON, + "Duplicate frame_start: " + std::to_string(frameStart) + " in file " + + jsonFile.filename().string()); + } + + // Create metadata item + MetadataItem metaItem(frameStart, frameDuration, std::move(payload), jsonFile.string()); + + LOG_DEBUG("Loaded SMPTE item {}: frame_start={}, frame_duration={}, payload_size={}", i, + frameStart, frameDuration, metaItem.payload.size()); + + // Insert into map + metadataMap[frameStart] = std::move(metaItem); + } + catch(const T35Exception &e) + { + // Re-throw T35 exceptions + throw; + } + catch(const std::exception &e) + { + LOG_WARN("Error processing {}: {}, skipping", jsonFile.filename().string(), e.what()); + continue; + } + } + + if(metadataMap.empty()) + { + throw T35Exception(T35Error::NoMetadataFound, + "No valid SMPTE metadata found in folder: " + path); + } + + LOG_INFO("Loaded {} SMPTE metadata items from {}", metadataMap.size(), path); + + // Validate the loaded metadata map + if(!validateMetadataMap(metadataMap, errorMsg)) + { + throw T35Exception(T35Error::ValidationFailed, errorMsg); + } + + return metadataMap; +} + +} // namespace t35 diff --git a/IsoLib/t35_tool/sources/SMPTEFolderSource.hpp b/IsoLib/t35_tool/sources/SMPTEFolderSource.hpp new file mode 100644 index 00000000..cbe8c7ad --- /dev/null +++ b/IsoLib/t35_tool/sources/SMPTEFolderSource.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include "MetadataSource.hpp" + +namespace t35 { + +/** + * SMPTE ST2094-50 folder source - reads folder with SMPTE JSON files + * + * Scans folder for .json files containing SMPTE ST2094-50 metadata + * Uses existing SMPTE_ST2094_50 encoding logic internally + */ +class SMPTEFolderSource : public MetadataSource { +public: + explicit SMPTEFolderSource(const std::string& folderPath); + virtual ~SMPTEFolderSource() = default; + + std::string getType() const override { return "smpte-folder"; } + MetadataMap load(const T35Prefix& prefix) override; + bool validate(std::string& errorMsg) override; + std::string getPath() const override { return path; } + +private: + std::string path; +}; + +} // namespace t35 diff --git a/IsoLib/t35_tool/sources/SMPTE_ST2094_50.cpp b/IsoLib/t35_tool/sources/SMPTE_ST2094_50.cpp new file mode 100644 index 00000000..7b47d59c --- /dev/null +++ b/IsoLib/t35_tool/sources/SMPTE_ST2094_50.cpp @@ -0,0 +1,1687 @@ +#include "SMPTE_ST2094_50.hpp" + +#include "SMPTE_ST2094_50.hpp" +#include +#include +#include +#include +#include + +#ifndef M_PI +#define M_PI 3.1415926535897932384626433832 +#endif + +/* *********************************** LOCAL LOGGING FUNCTIONS + * *******************************************************************************************/ + +// Local log level constants +#define LOGLEVEL_OFF 0 +#define LOGLEVEL_ERROR 1 +#define LOGLEVEL_WARNING 2 +#define LOGLEVEL_INFO 3 +#define LOGLEVEL_DEBUG 4 +#define LOGLEVEL_TRACE 5 + +// Global verbose level for this compilation unit +static int g_verboseLevel = LOGLEVEL_TRACE; + +// Local logging function - concatenates format and outputs to stdout +static void logMsg(int logLvl, const char *format, ...) +{ + // Skip if log level is higher than current verbose level + if(logLvl > g_verboseLevel) + { + return; + } + + // Print log level prefix + switch(logLvl) + { + case LOGLEVEL_ERROR: + std::printf("Error: "); + break; + case LOGLEVEL_WARNING: + std::printf("Warning: "); + break; + case LOGLEVEL_INFO: + std::printf("Info: "); + break; + case LOGLEVEL_DEBUG: + std::printf("Debug: "); + break; + case LOGLEVEL_TRACE: + std::printf("Trace: "); + break; + } + + // Print formatted message + va_list args; + va_start(args, format); + std::vprintf(format, args); + va_end(args); + + // Print newline + std::printf("\n"); +} + +/* *********************************** UTILITY FUNCTIONS + * *******************************************************************************************/ + +// Convert uint8 to heaxadecimal value +std::string uint8_to_hex(uint8_t value) +{ + std::stringstream ss; + ss << std::hex << std::setfill('0') << std::setw(2) << static_cast(value); + return ss.str(); +} + +// Formatted cout of a name and a value aligned for binary encoding and decoding debugging +void printDebug(const std::string &varName, uint16_t varValue, uint8_t nbBits, int verboseLevel) +{ + if(verboseLevel < LOGLEVEL_TRACE) + { + return; // Only print debug info at TRACE level + } + + // Explicitly set decimal output format + std::cout << std::dec; + + std::cout.width(50); + std::cout << varName << "="; + std::cout.width(6); + std::cout << varValue << " | "; + switch(nbBits) + { // bitset need constant + case 1: + std::cout.width(16); + std::cout << std::bitset<1>(varValue).to_string() << "\n"; + break; + case 2: + std::cout.width(16); + std::cout << std::bitset<2>(varValue).to_string() << "\n"; + break; + case 3: + std::cout.width(16); + std::cout << std::bitset<3>(varValue).to_string() << "\n"; + break; + case 4: + std::cout.width(16); + std::cout << std::bitset<4>(varValue).to_string() << "\n"; + break; + case 5: + std::cout.width(16); + std::cout << std::bitset<5>(varValue).to_string() << "\n"; + break; + case 6: + std::cout.width(16); + std::cout << std::bitset<6>(varValue).to_string() << "\n"; + break; + case 7: + std::cout.width(16); + std::cout << std::bitset<7>(varValue).to_string() << "\n"; + break; + case 8: + std::cout.width(16); + std::cout << std::bitset<8>(varValue).to_string() << "\n"; + break; + case 16: + std::cout.width(16); + std::cout << std::bitset<16>(varValue).to_string() << "\n"; + break; + default: + break; + } +} + +// Print binary data compatible with external verification tool +void printBinaryData(std::vector binary_data, int verboseLevel) +{ + if(verboseLevel < LOGLEVEL_TRACE) + { + return; // Only print debug info at TRACE level + } + + std::cout << "Binary data decoded, paste in external tool -> " + "https://ccameron-chromium.github.io/agtm-demo/parse.html" + << std::endl; + const int bytesPerRow = 16; // Define how many bytes per row you want + for(int i = 0; i < int(binary_data.size()); i++) + { + // Print hexadecimal value with 0x prefix + std::cout << "0x" << std::noshowbase << std::hex << static_cast(binary_data[i]) << ", "; + // New line after every 'bytesPerRow' bytes + if((i + 1) % bytesPerRow == 0) + { + std::cout << std::endl; + } + } + // Print a newline if the last row isn't complete + if(int(binary_data.size()) % bytesPerRow != 0) + { + std::cout << std::endl; + } +} + +void push_boolean(struct BinaryData *payloadBinaryData, bool boolValue, const std::string &varName, + int verboseLevel) +{ + uint8_t decValue = static_cast(boolValue); + payloadBinaryData->payload[payloadBinaryData->byteIdx] = + payloadBinaryData->payload[payloadBinaryData->byteIdx] + + (decValue << (7 - payloadBinaryData->bitIdx)); + printDebug(varName, decValue, 1, verboseLevel); + + payloadBinaryData->bitIdx++; + if(payloadBinaryData->bitIdx == uint8_t(8)) + { + payloadBinaryData->bitIdx = 0; + payloadBinaryData->payload.push_back(0); + payloadBinaryData->byteIdx++; + } + else if(payloadBinaryData->bitIdx > 8) + { + logMsg(LOGLEVEL_ERROR, "push_boolean exceeded a byte for %s", varName.c_str()); + } +} + +void push_bits(struct BinaryData *payloadBinaryData, uint8_t value, uint8_t nbBits, + const std::string &varName, int verboseLevel) +{ + payloadBinaryData->payload[payloadBinaryData->byteIdx] = + payloadBinaryData->payload[payloadBinaryData->byteIdx] + + (value << (8 - nbBits - payloadBinaryData->bitIdx)); + printDebug(varName, value, nbBits, verboseLevel); + payloadBinaryData->bitIdx += nbBits; + if(payloadBinaryData->bitIdx == 8) + { + payloadBinaryData->byteIdx++; + payloadBinaryData->bitIdx = 0; + payloadBinaryData->payload.push_back(0); + } + else if(payloadBinaryData->bitIdx > 8) + { + logMsg(LOGLEVEL_ERROR, "push_bits exceeded a byte for %s while trying to add %d bits", + varName.c_str(), nbBits); + } +} + +void push_8bits(struct BinaryData *payloadBinaryData, uint16_t value, const std::string &varName, + int verboseLevel) +{ + // Verify that we are at the start of a byte + if(payloadBinaryData->bitIdx != 0) + { + logMsg(LOGLEVEL_ERROR, "push_8bits called but we are not at the start of a byte"); + } + else + { + payloadBinaryData->payload[payloadBinaryData->byteIdx] = uint8_t(value & 0x00FF); + payloadBinaryData->payload.push_back(0); + payloadBinaryData->byteIdx++; + printDebug(varName, value, 8, verboseLevel); + } +} + +void push_16bits(struct BinaryData *payloadBinaryData, uint16_t value, const std::string &varName, + int verboseLevel) +{ + // Verify that we are at the start of a byte + if(payloadBinaryData->bitIdx != 0) + { + logMsg(LOGLEVEL_ERROR, "push_16bits called but we are not at the start of a byte"); + } + else + { + payloadBinaryData->payload[payloadBinaryData->byteIdx] = uint8_t((value >> 8) & 0x00FF); + payloadBinaryData->payload.push_back(0); + payloadBinaryData->byteIdx++; + payloadBinaryData->payload[payloadBinaryData->byteIdx] = uint8_t((value) & 0x00FF); + payloadBinaryData->payload.push_back(0); + payloadBinaryData->byteIdx++; + printDebug(varName, value, 16, verboseLevel); + } +} + +bool pull_boolean(struct BinaryData *payloadBinaryData, const std::string &varName, + int verboseLevel) +{ + uint8_t decValue = + (payloadBinaryData->payload[payloadBinaryData->byteIdx] >> (7 - payloadBinaryData->bitIdx)) & + 0x01; + bool result = static_cast(decValue); + payloadBinaryData->bitIdx++; + if(payloadBinaryData->bitIdx == uint8_t(8)) + { + payloadBinaryData->byteIdx++; + payloadBinaryData->bitIdx = 0; + } + else if(payloadBinaryData->bitIdx > 8) + { + logMsg(LOGLEVEL_ERROR, "pull_boolean exceeded a byte for %s", varName.c_str()); + } + printDebug(varName, decValue, 1, verboseLevel); + return result; +} + +uint16_t pull_bits(struct BinaryData *payloadBinaryData, uint8_t nbBits, const std::string &varName, + int verboseLevel) +{ + uint8_t decValue = + uint8_t(payloadBinaryData->payload[payloadBinaryData->byteIdx] << payloadBinaryData->bitIdx) >> + ((8 - nbBits)); + payloadBinaryData->bitIdx += nbBits; + if(payloadBinaryData->bitIdx == 8) + { + payloadBinaryData->byteIdx++; + payloadBinaryData->bitIdx = 0; + } + else if(payloadBinaryData->bitIdx > 8) + { + logMsg(LOGLEVEL_ERROR, "pull_bits exceeded a byte for %s while trying to add %d bits", + varName.c_str(), nbBits); + } + printDebug(varName, decValue, nbBits, verboseLevel); + return uint16_t(decValue); +} + +uint16_t pull_8bits(struct BinaryData *payloadBinaryData, const std::string &varName, + int verboseLevel) +{ + // Verify that we are at the start of a byte + uint16_t decValue = 404; + if(payloadBinaryData->bitIdx != 0) + { + logMsg(LOGLEVEL_ERROR, "pull_8bits called but we are not at the start of a byte"); + } + else + { + decValue = uint16_t(payloadBinaryData->payload[payloadBinaryData->byteIdx]); + } + printDebug(varName, decValue, 8, verboseLevel); + payloadBinaryData->byteIdx++; + return decValue; +} + +uint16_t pull_16bits(struct BinaryData *payloadBinaryData, const std::string &varName, + int verboseLevel) +{ + // Verify that we are at the start of a byte + uint16_t decValue = 404; + if(payloadBinaryData->bitIdx != 0) + { + logMsg(LOGLEVEL_ERROR, "pull_16bits called but we are not at the start of a byte"); + } + else + { + decValue = uint16_t(payloadBinaryData->payload[payloadBinaryData->byteIdx]) << 8; + payloadBinaryData->byteIdx++; + decValue = decValue + uint16_t(payloadBinaryData->payload[payloadBinaryData->byteIdx]); + payloadBinaryData->byteIdx++; + printDebug(varName, decValue, 16, verboseLevel); + } + return decValue; +} + +/* *********************************** SMPTE ST 2094-50 FUNCTIONS + * *******************************************************************************************/ +// Constructors +SMPTE_ST2094_50::SMPTE_ST2094_50() +{ + keyValue = "B500900001:SMPTE-ST2094-50"; + + // Application - fixed + applicationIdentifier = 5; + applicationVersion = 255; + + // ProcessingWindow - fixed + pWin.upperLeftCorner = 0; + pWin.lowerRightCorner = 0; + pWin.windowNumber = 1; + + // Initialize convenience flags + isHeadroomAdaptiveToneMap = false; + isReferenceWhiteToneMapping = false; + for(uint16_t iAlt = 0; iAlt < MAX_NB_ALTERNATE; iAlt++) + { + hasSlopeParameter[iAlt] = false; + } + + // Initialize verbose level to INFO (default) + verboseLevel = LOGLEVEL_TRACE; + g_verboseLevel = LOGLEVEL_TRACE; // Also set the global +} + +// Getters +std::vector SMPTE_ST2094_50::getPayloadData() { return payloadBinaryData.payload; } +uint32_t SMPTE_ST2094_50::getTimeIntervalStart() { return timeI.timeIntervalStart; } +uint32_t SMPTE_ST2094_50::getTimeintervalDuration() { return timeI.timeintervalDuration; } + +// Setters +void SMPTE_ST2094_50::setTimeIntervalStart(uint32_t frame_start) +{ + timeI.timeIntervalStart = frame_start; +} +void SMPTE_ST2094_50::setTimeintervalDuration(uint32_t frame_duration) +{ + timeI.timeintervalDuration = frame_duration; +} +void SMPTE_ST2094_50::setVerboseLevel(int level) +{ + if(level >= LOGLEVEL_OFF && level <= LOGLEVEL_TRACE) + { + verboseLevel = level; + g_verboseLevel = level; // Update the global for logMsg + } + else + { + logMsg(LOGLEVEL_WARNING, "Invalid verbose level %d, keeping current level %d", level, + verboseLevel); + } +} + +/* *********************************** ENCODING SECTION + * ********************************************************************************************/ +// Read from json file the metadata items +/* *********************************** ENCODING SECTION + * ********************************************************************************************/ +// Read from json file the metadata items +bool SMPTE_ST2094_50::decodeJsonToMetadataItems(nlohmann::json j) +{ + logMsg(LOGLEVEL_DEBUG, "DECODE JSON TO METADATA ITEMS"); + + if(j.is_null() || !j.is_object()) + { + logMsg(LOGLEVEL_ERROR, "Invalid JSON dictionary"); + return true; + } + + // Check if there's a top-level SMPTEST2094_50 wrapper + nlohmann::json rootDict = j; + if(j.contains("SMPTEST2094_50")) + { + rootDict = j["SMPTEST2094_50"]; + if(!rootDict.is_object()) + { + logMsg(LOGLEVEL_ERROR, "SMPTEST2094_50 is not a dictionary"); + return true; + } + } + else + { + logMsg(LOGLEVEL_ERROR, "SMPTEST2094_50 not found in json file"); + } + + // Parse top-level fields + if(rootDict.contains("frameStart")) + { + timeI.timeIntervalStart = rootDict["frameStart"].get(); + } + + if(rootDict.contains("frameDuration")) + { + timeI.timeintervalDuration = rootDict["frameDuration"].get(); + } + + if(rootDict.contains("windowNumber")) + { + pWin.windowNumber = rootDict["windowNumber"].get(); + } + + // Extract ColorVolumeTransform dictionary + if(!rootDict.contains("ColorVolumeTransform") || !rootDict["ColorVolumeTransform"].is_object()) + { + logMsg(LOGLEVEL_ERROR, "ColorVolumeTransform dictionary missing or invalid"); + return true; + } + + nlohmann::json cvtDict = rootDict["ColorVolumeTransform"]; + + // Parse hdrReferenceWhite (required field) + if(!cvtDict.contains("hdrReferenceWhite")) + { + logMsg(LOGLEVEL_ERROR, "hdrReferenceWhite metadata item missing"); + return true; + } + + try + { + cvt.hdrReferenceWhite = cvtDict["hdrReferenceWhite"].get(); + } + catch(const std::exception &e) + { + logMsg(LOGLEVEL_ERROR, "Failed to parse 'hdrReferenceWhite': %s", e.what()); + return true; + } + + // Extract HeadroomAdaptiveToneMapping dictionary (optional) + if(!cvtDict.contains("HeadroomAdaptiveToneMapping") || + !cvtDict["HeadroomAdaptiveToneMapping"].is_object()) + { + // No HATM - this is reference white tone mapping only + isHeadroomAdaptiveToneMap = false; + isReferenceWhiteToneMapping = false; + return false; + } + + nlohmann::json hatmDict = cvtDict["HeadroomAdaptiveToneMapping"]; + + // HATM is present + isHeadroomAdaptiveToneMap = true; + + // Parse baselineHdrHeadroom + if(hatmDict.contains("baselineHdrHeadroom")) + { + cvt.hatm.baselineHdrHeadroom = hatmDict["baselineHdrHeadroom"].get(); + } + + // Parse numAlternateImages + if(!hatmDict.contains("numAlternateImages")) + { + // If numAlternateImages is not present, assume reference white tone mapping + isReferenceWhiteToneMapping = true; + return false; + } + + cvt.hatm.numAlternateImages = hatmDict["numAlternateImages"].get(); + isReferenceWhiteToneMapping = false; + + // Parse gainApplicationSpaceChromaticities (optional) + if(hatmDict.contains("gainApplicationSpaceChromaticities")) + { + std::vector gainAppSpaceChrom = + hatmDict["gainApplicationSpaceChromaticities"].get>(); + if(gainAppSpaceChrom.size() != MAX_NB_CHROMATICITIES) + { + logMsg(LOGLEVEL_ERROR, "gainApplicationSpaceChromaticities array size (%zu) != %d", + gainAppSpaceChrom.size(), MAX_NB_CHROMATICITIES); + return true; + } + for(int iCh = 0; iCh < MAX_NB_CHROMATICITIES; iCh++) + { + cvt.hatm.gainApplicationSpaceChromaticities[iCh] = gainAppSpaceChrom[iCh]; + } + } + + if(cvt.hatm.numAlternateImages >= 1) + { + // Parse alternateHdrHeadroom array + if(hatmDict.contains("alternateHdrHeadroom")) + { + nlohmann::json alternateHdrHeadroomValue = hatmDict["alternateHdrHeadroom"]; + std::vector alternateHdrHeadroomArray; + + // Convert to array if it's a single value + if(alternateHdrHeadroomValue.is_number()) + { + // Single value - wrap it in an array + alternateHdrHeadroomArray.push_back(alternateHdrHeadroomValue.get()); + logMsg(LOGLEVEL_DEBUG, "alternateHdrHeadroom is a single value, converted to array"); + } + else if(alternateHdrHeadroomValue.is_array()) + { + alternateHdrHeadroomArray = alternateHdrHeadroomValue.get>(); + } + else + { + logMsg(LOGLEVEL_ERROR, "alternateHdrHeadroom is neither an array nor a number, skipping"); + return true; + } + + if(!alternateHdrHeadroomArray.empty()) + { + // Validate array count matches numAlternateImages + if(alternateHdrHeadroomArray.size() != cvt.hatm.numAlternateImages) + { + logMsg(LOGLEVEL_ERROR, + "alternateHdrHeadroom array count (%zu) does not match numAlternateImages (%u)", + alternateHdrHeadroomArray.size(), cvt.hatm.numAlternateImages); + return true; + } + + // Populate alternateHdrHeadroom values + for(uint32_t i = 0; i < cvt.hatm.numAlternateImages; i++) + { + cvt.hatm.alternateHdrHeadroom.push_back(alternateHdrHeadroomArray[i]); + } + } + } + + // Parse ColorGainFunction array + if(!hatmDict.contains("ColorGainFunction")) + { + logMsg(LOGLEVEL_ERROR, "ColorGainFunction missing"); + return true; + } + + nlohmann::json colorGainFunctionValue = hatmDict["ColorGainFunction"]; + std::vector colorGainFunctionArray; + + // Convert to array if it's a single dictionary + if(colorGainFunctionValue.is_object()) + { + // Single ColorGainFunction dictionary - wrap it in an array + colorGainFunctionArray.push_back(colorGainFunctionValue); + logMsg(LOGLEVEL_DEBUG, "ColorGainFunction is a single dictionary, converted to array"); + } + else if(colorGainFunctionValue.is_array()) + { + colorGainFunctionArray = colorGainFunctionValue.get>(); + } + else + { + logMsg(LOGLEVEL_ERROR, "ColorGainFunction is neither an array nor a dictionary"); + return true; + } + + // Validate array count matches numAlternateImages + if(colorGainFunctionArray.size() != cvt.hatm.numAlternateImages) + { + logMsg(LOGLEVEL_ERROR, + "ColorGainFunction array count (%zu) does not match numAlternateImages (%u)", + colorGainFunctionArray.size(), cvt.hatm.numAlternateImages); + return true; + } + + // Parse each ColorGainFunction + for(uint32_t i = 0; i < cvt.hatm.numAlternateImages; i++) + { + nlohmann::json cgfDict = colorGainFunctionArray[i]; + if(!cgfDict.is_object()) + { + logMsg(LOGLEVEL_ERROR, "ColorGainFunction[%u] is not a dictionary", i); + return true; + } + + ColorGainFunction cgf; + + // Parse ComponentMix + if(!cgfDict.contains("ComponentMix") || !cgfDict["ComponentMix"].is_object()) + { + logMsg(LOGLEVEL_ERROR, "ComponentMix dictionary missing for ColorGainFunction[%u]", i); + return true; + } + + nlohmann::json componentMixDict = cgfDict["ComponentMix"]; + cgf.cm.componentMixRed = componentMixDict.value("componentMixRed", 0.0f); + cgf.cm.componentMixGreen = componentMixDict.value("componentMixGreen", 0.0f); + cgf.cm.componentMixBlue = componentMixDict.value("componentMixBlue", 0.0f); + cgf.cm.componentMixMax = componentMixDict.value("componentMixMax", 0.0f); + cgf.cm.componentMixMin = componentMixDict.value("componentMixMin", 0.0f); + cgf.cm.componentMixComponent = componentMixDict.value("componentMixComponent", 0.0f); + + // Parse GainCurve + if(!cgfDict.contains("GainCurve") || !cgfDict["GainCurve"].is_object()) + { + logMsg(LOGLEVEL_ERROR, "GainCurve dictionary missing for ColorGainFunction[%u]", i); + return false; + } + + nlohmann::json gainCurveDict = cgfDict["GainCurve"]; + + // Parse gainCurveNumControlPoints + if(!gainCurveDict.contains("gainCurveNumControlPoints")) + { + logMsg(LOGLEVEL_ERROR, "gainCurveNumControlPoints missing for ColorGainFunction[%u]", i); + return false; + } + + cgf.gc.gainCurveNumControlPoints = gainCurveDict["gainCurveNumControlPoints"].get(); + + // Helper lambda to handle single value or array + auto parseControlPointArray = [&](const std::string &key, std::vector &target) -> bool + { + if(!gainCurveDict.contains(key)) + { + return false; + } + + nlohmann::json value = gainCurveDict[key]; + if(value.is_array()) + { + std::vector arr = value.get>(); + if(arr.size() != cgf.gc.gainCurveNumControlPoints) + { + logMsg(LOGLEVEL_ERROR, "%s array count mismatch for ColorGainFunction[%u]", key.c_str(), + i); + return false; + } + target = arr; + } + else if(value.is_number()) + { + // Single value - replicate it + float singleValue = value.get(); + target.resize(cgf.gc.gainCurveNumControlPoints, singleValue); + } + return true; + }; + + // Parse X control points + if(!parseControlPointArray("gainCurveControlPointX", cgf.gc.gainCurveControlPointX)) + { + logMsg(LOGLEVEL_ERROR, "Failed to parse gainCurveControlPointX for ColorGainFunction[%u]", + i); + return false; + } + + // Parse Y control points + if(!parseControlPointArray("gainCurveControlPointY", cgf.gc.gainCurveControlPointY)) + { + logMsg(LOGLEVEL_ERROR, "Failed to parse gainCurveControlPointY for ColorGainFunction[%u]", + i); + return true; + } + + // Parse Slope M control points (optional) + if(gainCurveDict.contains("gainCurveControlPointM")) + { + hasSlopeParameter[i] = true; + if(!parseControlPointArray("gainCurveControlPointM", cgf.gc.gainCurveControlPointM)) + { + logMsg(LOGLEVEL_WARNING, + "Failed to parse gainCurveControlPointM for ColorGainFunction[%u], continuing " + "without it", + i); + hasSlopeParameter[i] = false; + } + } + else + { + hasSlopeParameter[i] = false; + } + + cvt.hatm.cgf.push_back(cgf); + } + } + return false; +} + +// Convert metadata items to syntax elements +void SMPTE_ST2094_50::convertMetadataItemsToSyntaxElements() +{ + elm.has_custom_hdr_reference_white_flag = false; + elm.has_adaptive_tone_map_flag = false; + if(std::abs(cvt.hdrReferenceWhite - 203.0) > (0.5f * Q_HDR_REFERENCE_WHITE)) + { + elm.has_custom_hdr_reference_white_flag = true; + elm.hdr_reference_white = uint16_t(cvt.hdrReferenceWhite * Q_HDR_REFERENCE_WHITE); + } + if(isHeadroomAdaptiveToneMap) + { + elm.has_adaptive_tone_map_flag = true; + elm.use_reference_white_tone_mapping_flag = true; + elm.baseline_hdr_headroom = uint16_t(cvt.hatm.baselineHdrHeadroom * Q_HDR_HEADROOM + 0.5f); + elm.use_reference_white_tone_mapping_flag = isReferenceWhiteToneMapping; + if(!isReferenceWhiteToneMapping) + { + elm.use_reference_white_tone_mapping_flag = false; + elm.num_alternate_images = uint16_t(cvt.hatm.numAlternateImages); + + if(cvt.hatm.cgf.size() < cvt.hatm.numAlternateImages) + { + logMsg(LOGLEVEL_ERROR, + "cgf array size (%zu) is less than numAlternateImages (%u). JSON data is incomplete " + "or malformed.", + cvt.hatm.cgf.size(), cvt.hatm.numAlternateImages); + return; + } + + // Check if the primary combination is known + if(std::abs(cvt.hatm.gainApplicationSpaceChromaticities[0] - 0.64) < + P_GAIN_APPLICATION_SPACE_CHROMATICITY && + std::abs(cvt.hatm.gainApplicationSpaceChromaticities[1] - 0.33) < + P_GAIN_APPLICATION_SPACE_CHROMATICITY && + std::abs(cvt.hatm.gainApplicationSpaceChromaticities[2] - 0.30) < + P_GAIN_APPLICATION_SPACE_CHROMATICITY && + std::abs(cvt.hatm.gainApplicationSpaceChromaticities[3] - 0.60) < + P_GAIN_APPLICATION_SPACE_CHROMATICITY && + std::abs(cvt.hatm.gainApplicationSpaceChromaticities[4] - 0.15) < + P_GAIN_APPLICATION_SPACE_CHROMATICITY && + std::abs(cvt.hatm.gainApplicationSpaceChromaticities[5] - 0.06) < + P_GAIN_APPLICATION_SPACE_CHROMATICITY && + std::abs(cvt.hatm.gainApplicationSpaceChromaticities[6] - 0.3127) < + P_GAIN_APPLICATION_SPACE_CHROMATICITY && + std::abs(cvt.hatm.gainApplicationSpaceChromaticities[7] - 0.3290) < + P_GAIN_APPLICATION_SPACE_CHROMATICITY) + { + elm.gain_application_space_chromaticities_mode = 0; + } + else if(std::abs(cvt.hatm.gainApplicationSpaceChromaticities[0] - 0.68) < + P_GAIN_APPLICATION_SPACE_CHROMATICITY && + std::abs(cvt.hatm.gainApplicationSpaceChromaticities[1] - 0.32) < + P_GAIN_APPLICATION_SPACE_CHROMATICITY && + std::abs(cvt.hatm.gainApplicationSpaceChromaticities[2] - 0.265) < + P_GAIN_APPLICATION_SPACE_CHROMATICITY && + std::abs(cvt.hatm.gainApplicationSpaceChromaticities[3] - 0.69) < + P_GAIN_APPLICATION_SPACE_CHROMATICITY && + std::abs(cvt.hatm.gainApplicationSpaceChromaticities[4] - 0.15) < + P_GAIN_APPLICATION_SPACE_CHROMATICITY && + std::abs(cvt.hatm.gainApplicationSpaceChromaticities[5] - 0.06) < + P_GAIN_APPLICATION_SPACE_CHROMATICITY && + std::abs(cvt.hatm.gainApplicationSpaceChromaticities[6] - 0.3127) < + P_GAIN_APPLICATION_SPACE_CHROMATICITY && + std::abs(cvt.hatm.gainApplicationSpaceChromaticities[7] - 0.3290) < + P_GAIN_APPLICATION_SPACE_CHROMATICITY) + { + elm.gain_application_space_chromaticities_mode = 1; + } + else if(std::abs(cvt.hatm.gainApplicationSpaceChromaticities[0] - 0.708) < + P_GAIN_APPLICATION_SPACE_CHROMATICITY && + std::abs(cvt.hatm.gainApplicationSpaceChromaticities[1] - 0.292) < + P_GAIN_APPLICATION_SPACE_CHROMATICITY && + std::abs(cvt.hatm.gainApplicationSpaceChromaticities[2] - 0.17) < + P_GAIN_APPLICATION_SPACE_CHROMATICITY && + std::abs(cvt.hatm.gainApplicationSpaceChromaticities[3] - 0.797) < + P_GAIN_APPLICATION_SPACE_CHROMATICITY && + std::abs(cvt.hatm.gainApplicationSpaceChromaticities[4] - 0.131) < + P_GAIN_APPLICATION_SPACE_CHROMATICITY && + std::abs(cvt.hatm.gainApplicationSpaceChromaticities[5] - 0.046) < + P_GAIN_APPLICATION_SPACE_CHROMATICITY && + std::abs(cvt.hatm.gainApplicationSpaceChromaticities[6] - 0.3127) < + P_GAIN_APPLICATION_SPACE_CHROMATICITY && + std::abs(cvt.hatm.gainApplicationSpaceChromaticities[7] - 0.3290) < + P_GAIN_APPLICATION_SPACE_CHROMATICITY) + { + elm.gain_application_space_chromaticities_mode = 2; + } + else + { + elm.gain_application_space_chromaticities_mode = 3; + for(uint16_t iCh = 0; iCh < 8; iCh++) + { + elm.gain_application_space_chromaticities[iCh] = + uint16_t(cvt.hatm.gainApplicationSpaceChromaticities[iCh] * + Q_GAIN_APPLICATION_SPACE_CHROMATICITY + + 0.5f); + } + } + + // Validate that we have the required data structures + if(cvt.hatm.numAlternateImages == 0) + { + logMsg(LOGLEVEL_ERROR, "numAlternateImages is 0, skipping the rest of metadata items"); + return; + } + + // Loop over alternate images + elm.has_common_component_mix_params_flag = + true; // Check if all component mixing uses the same parameters + elm.has_common_curve_params_flag = true; // Check if all alternate have the same number of + // control points, x position and interpolation + for(uint16_t iAlt = 0; iAlt < cvt.hatm.numAlternateImages; iAlt++) + { + if(fabs(cvt.hatm.alternateHdrHeadroom[iAlt] - cvt.hatm.baselineHdrHeadroom) < + P_HDR_HEADROOM) + { + logMsg(LOGLEVEL_ERROR, "alternateHdrHeadroom[%d] cannot be equal to baselineHdrHeadroom", + iAlt); + return; + } + elm.alternate_hdr_headrooms[iAlt] = + uint16_t(cvt.hatm.alternateHdrHeadroom[iAlt] * Q_HDR_HEADROOM + 0.5f); + + // init coefficient to 0 + for(uint16_t iCmf = 0; iCmf < MAX_NB_COMPONENT_MIXING_COEFFICIENT; iCmf++) + { + elm.component_mixing_coefficient[iAlt][iCmf] = uint16_t(0); + elm.has_component_mixing_coefficient_flag[iAlt][iCmf] = false; + } + // Component mixing + if(std::abs(cvt.hatm.cgf[iAlt].cm.componentMixRed) < P_COMPONENT_MIXING_COEFFICIENT && + std::abs(cvt.hatm.cgf[iAlt].cm.componentMixGreen) < P_COMPONENT_MIXING_COEFFICIENT && + std::abs(cvt.hatm.cgf[iAlt].cm.componentMixBlue) < P_COMPONENT_MIXING_COEFFICIENT && + std::abs(cvt.hatm.cgf[iAlt].cm.componentMixMax - 1.0) < P_COMPONENT_MIXING_COEFFICIENT && + std::abs(cvt.hatm.cgf[iAlt].cm.componentMixMin) < P_COMPONENT_MIXING_COEFFICIENT && + std::abs(cvt.hatm.cgf[iAlt].cm.componentMixComponent) < P_COMPONENT_MIXING_COEFFICIENT) + { + elm.component_mixing_type[iAlt] = 0; + } + else if(std::abs(cvt.hatm.cgf[iAlt].cm.componentMixRed) < P_COMPONENT_MIXING_COEFFICIENT && + std::abs(cvt.hatm.cgf[iAlt].cm.componentMixGreen) < + P_COMPONENT_MIXING_COEFFICIENT && + std::abs(cvt.hatm.cgf[iAlt].cm.componentMixBlue) < P_COMPONENT_MIXING_COEFFICIENT && + std::abs(cvt.hatm.cgf[iAlt].cm.componentMixMax) < P_COMPONENT_MIXING_COEFFICIENT && + std::abs(cvt.hatm.cgf[iAlt].cm.componentMixMin) < P_COMPONENT_MIXING_COEFFICIENT && + std::abs(cvt.hatm.cgf[iAlt].cm.componentMixComponent - 1.0) < + P_COMPONENT_MIXING_COEFFICIENT) + { + elm.component_mixing_type[iAlt] = 1; + } + else if(std::abs(cvt.hatm.cgf[iAlt].cm.componentMixRed - (1.0 / 6.0)) < + P_COMPONENT_MIXING_COEFFICIENT && + std::abs(cvt.hatm.cgf[iAlt].cm.componentMixGreen - (1.0 / 6.0)) < + P_COMPONENT_MIXING_COEFFICIENT && + std::abs(cvt.hatm.cgf[iAlt].cm.componentMixBlue - (1.0 / 6.0)) < + P_COMPONENT_MIXING_COEFFICIENT && + std::abs(cvt.hatm.cgf[iAlt].cm.componentMixMax - (1.0 / 2.0)) < + P_COMPONENT_MIXING_COEFFICIENT && + std::abs(cvt.hatm.cgf[iAlt].cm.componentMixMin) < P_COMPONENT_MIXING_COEFFICIENT && + std::abs(cvt.hatm.cgf[iAlt].cm.componentMixComponent) < + P_COMPONENT_MIXING_COEFFICIENT) + { + elm.component_mixing_type[iAlt] = 2; + } + else + { // Send flag to true for each non-zero coefficient + elm.component_mixing_type[iAlt] = 3; + uint16_t iCmf = 0; + elm.component_mixing_coefficient[iAlt][iCmf] = + uint16_t(cvt.hatm.cgf[iAlt].cm.componentMixRed * Q_COMPONENT_MIXING_COEFFICIENT + 0.5f); + iCmf++; + elm.component_mixing_coefficient[iAlt][iCmf] = uint16_t( + cvt.hatm.cgf[iAlt].cm.componentMixGreen * Q_COMPONENT_MIXING_COEFFICIENT + 0.5f); + iCmf++; + elm.component_mixing_coefficient[iAlt][iCmf] = uint16_t( + cvt.hatm.cgf[iAlt].cm.componentMixBlue * Q_COMPONENT_MIXING_COEFFICIENT + 0.5f); + iCmf++; + elm.component_mixing_coefficient[iAlt][iCmf] = + uint16_t(cvt.hatm.cgf[iAlt].cm.componentMixMax * Q_COMPONENT_MIXING_COEFFICIENT + 0.5f); + iCmf++; + elm.component_mixing_coefficient[iAlt][iCmf] = + uint16_t(cvt.hatm.cgf[iAlt].cm.componentMixMin * Q_COMPONENT_MIXING_COEFFICIENT + 0.5f); + iCmf++; + elm.component_mixing_coefficient[iAlt][iCmf] = uint16_t( + cvt.hatm.cgf[iAlt].cm.componentMixComponent * Q_COMPONENT_MIXING_COEFFICIENT + 0.5f); + iCmf++; + uint16_t sumCoefficients = 0; + for(iCmf = 0; iCmf < MAX_NB_COMPONENT_MIXING_COEFFICIENT; iCmf++) + { + if(elm.component_mixing_coefficient[iAlt][iCmf] > 0 && + elm.component_mixing_coefficient[iAlt][iCmf] <= Q_COMPONENT_MIXING_COEFFICIENT) + { + elm.has_component_mixing_coefficient_flag[iAlt][iCmf] = true; + } + else if(elm.component_mixing_coefficient[iAlt][iCmf] == 0) + { + elm.has_component_mixing_coefficient_flag[iAlt][iCmf] = false; + } + else + { + logMsg( + LOGLEVEL_ERROR, + "component mixing coefficient for alternate %d color %d is greater than 1.0 (%d)", + iAlt, iCmf, + (float)elm.component_mixing_coefficient[iAlt][iCmf] / + Q_COMPONENT_MIXING_COEFFICIENT); + } + // Check if same mode as alternate 0 and same coefficient, if not, then not common + if(elm.component_mixing_type[0] == 3 && (elm.component_mixing_coefficient[0][iCmf] != + elm.component_mixing_coefficient[iAlt][iCmf])) + { + elm.has_common_component_mix_params_flag = false; + } + sumCoefficients = sumCoefficients + elm.component_mixing_coefficient[iAlt][iCmf]; + } + if(sumCoefficients != Q_COMPONENT_MIXING_COEFFICIENT) + { + logMsg(LOGLEVEL_WARNING, + "Sum component mixing coefficient for alternate %d is not equal to 1.0, they " + "will be scaled to 1.0 at decoding", + iAlt); + } + } + if(elm.component_mixing_type[0] != elm.component_mixing_type[iAlt]) + { + elm.has_common_component_mix_params_flag = false; + } + + // Create syntax elements for the gain curve function + elm.gain_curve_num_control_points_minus_1[iAlt] = + cvt.hatm.cgf[iAlt].gc.gainCurveNumControlPoints - 1; + if(elm.gain_curve_num_control_points_minus_1[0] != + elm.gain_curve_num_control_points_minus_1[iAlt]) + { + elm.has_common_curve_params_flag = false; + } + + for(uint16_t iCps = 0; iCps < cvt.hatm.cgf[iAlt].gc.gainCurveNumControlPoints; iCps++) + { + elm.gain_curve_control_points_x[iAlt][iCps] = uint16_t( + cvt.hatm.cgf[iAlt].gc.gainCurveControlPointX[iCps] * Q_GAIN_CURVE_CONTROL_POINT_X + + 0.5f); + if(elm.gain_curve_control_points_x[0][iCps] != + elm.gain_curve_control_points_x[iAlt][iCps]) + { + elm.has_common_curve_params_flag = false; + } + + elm.gain_curve_control_points_y[iAlt][iCps] = + uint16_t(std::abs(cvt.hatm.cgf[iAlt].gc.gainCurveControlPointY[iCps]) * + Q_GAIN_CURVE_CONTROL_POINT_Y + + 0.5f); + } + elm.gain_curve_use_pchip_slope_flag[iAlt] = !hasSlopeParameter[iAlt]; + if(elm.gain_curve_use_pchip_slope_flag[0] != elm.gain_curve_use_pchip_slope_flag[iAlt]) + { + elm.has_common_curve_params_flag = false; + } + if(!elm.gain_curve_use_pchip_slope_flag[iAlt]) + { + for(uint16_t iCps = 0; iCps < cvt.hatm.cgf[iAlt].gc.gainCurveNumControlPoints; iCps++) + { + float theta = atan(cvt.hatm.cgf[iAlt].gc.gainCurveControlPointM[iCps]) * 180.0f / M_PI; + elm.gain_curve_control_points_theta[iAlt][iCps] = uint16_t( + (theta + O_GAIN_CURVE_CONTROL_POINT_THETA) * Q_GAIN_CURVE_CONTROL_POINT_THETA + 0.5f); + } + } + } + } + } +} + +// Convert syntax element to finary data and write to file +void SMPTE_ST2094_50::writeSyntaxElementsToBinaryData() +{ + // ================================================= Convert binary data from Syntax Elements + // =================================== Initialize the binary payload structure + payloadBinaryData.byteIdx = 0; + payloadBinaryData.bitIdx = 0; + payloadBinaryData.payload.push_back(0); + + logMsg(LOGLEVEL_DEBUG, "Start SMPTE_ST2094_50::writeSyntaxElementsToBinaryData"); + push_bits(&payloadBinaryData, elm.application_version, 3, "application_version", verboseLevel); + push_bits(&payloadBinaryData, elm.minimum_application_version, 3, "minimum_application_version", + verboseLevel); + push_bits(&payloadBinaryData, 0, 2, "zero_2bits", verboseLevel); + + push_boolean(&payloadBinaryData, elm.has_custom_hdr_reference_white_flag, + "has_custom_hdr_reference_white_flag", verboseLevel); + push_boolean(&payloadBinaryData, elm.has_adaptive_tone_map_flag, "has_adaptive_tone_map_flag", + verboseLevel); + push_bits(&payloadBinaryData, 0, 6, "zero_6bits", verboseLevel); + + if(elm.has_custom_hdr_reference_white_flag) + { + push_16bits(&payloadBinaryData, elm.hdr_reference_white, "hdr_reference_white", verboseLevel); + } + + if(elm.has_adaptive_tone_map_flag) + { + push_16bits(&payloadBinaryData, elm.baseline_hdr_headroom, "baseline_hdr_headroom", + verboseLevel); + push_boolean(&payloadBinaryData, elm.use_reference_white_tone_mapping_flag, + "use_reference_white_tone_mapping_flag", verboseLevel); + if(!elm.use_reference_white_tone_mapping_flag) + { + push_bits(&payloadBinaryData, uint8_t(elm.num_alternate_images), 3, "num_alternate_images", + verboseLevel); + push_bits(&payloadBinaryData, uint8_t(elm.gain_application_space_chromaticities_mode), 2, + "gain_application_space_chromaticities_mode", verboseLevel); + push_boolean(&payloadBinaryData, elm.has_common_component_mix_params_flag, + "has_common_component_mix_params_flag", verboseLevel); + push_boolean(&payloadBinaryData, elm.has_common_curve_params_flag, + "has_common_curve_params_flag", verboseLevel); + + if(elm.gain_application_space_chromaticities_mode == 3) + { + for(uint16_t iCh = 0; iCh < 8; iCh++) + { + push_16bits(&payloadBinaryData, elm.gain_application_space_chromaticities[iCh], + "gain_application_space_chromaticities[iCh]", verboseLevel); + } + } + + for(uint16_t iAlt = 0; iAlt < elm.num_alternate_images; iAlt++) + { + push_16bits(&payloadBinaryData, elm.alternate_hdr_headrooms[iAlt], + "alternate_hdr_headrooms[iAlt]", verboseLevel); + // Write component mixing function parameters + if(iAlt == 0 || !elm.has_common_component_mix_params_flag) + { + push_bits(&payloadBinaryData, uint8_t(elm.component_mixing_type[iAlt]), 2, + "component_mixing_type[iAlt]", verboseLevel); + if(elm.component_mixing_type[iAlt] == 3) + { + // Write the flag to indicate which coefficients are signaled + uint8_t value_8 = 0; + for(uint8_t iCm = 0; iCm < MAX_NB_COMPONENT_MIXING_COEFFICIENT; iCm++) + { + uint8_t flagValue = + static_cast(elm.has_component_mixing_coefficient_flag[iAlt][iCm]); + value_8 = value_8 + (flagValue << (5 - iCm)); + } + push_bits(&payloadBinaryData, value_8, 6, "has_component_mixing_coefficient_flag[iAlt]", + verboseLevel); + // Write the coefficients + for(uint8_t iCm = 0; iCm < MAX_NB_COMPONENT_MIXING_COEFFICIENT; iCm++) + { + if(elm.has_component_mixing_coefficient_flag[iAlt][iCm]) + { + push_16bits(&payloadBinaryData, elm.component_mixing_coefficient[iAlt][iCm], + "component_mixing_coefficient[iAlt][iCm]", verboseLevel); + } + } + } + else + { + push_bits(&payloadBinaryData, 0, 6, "zero_6bits[iAlt][iCm]", verboseLevel); + } + } + /// Write gain curve function parameters + if(iAlt == 0 || !elm.has_common_curve_params_flag) + { + push_bits(&payloadBinaryData, elm.gain_curve_num_control_points_minus_1[iAlt], 5, + "gain_curve_num_control_points_minus_1[iAlt]", verboseLevel); + push_boolean(&payloadBinaryData, elm.gain_curve_use_pchip_slope_flag[iAlt], + "gain_curve_use_pchip_slope_flag[iAlt]", verboseLevel); + push_bits(&payloadBinaryData, 0, 2, "zero_2bits[iAlt]", verboseLevel); + for(uint16_t iCps = 0; iCps < elm.gain_curve_num_control_points_minus_1[iAlt] + 1; iCps++) + { + push_16bits(&payloadBinaryData, elm.gain_curve_control_points_x[iAlt][iCps], + "gain_curve_control_points_x[iAlt][iCps]", verboseLevel); + } + } + for(uint16_t iCps = 0; iCps < elm.gain_curve_num_control_points_minus_1[iAlt] + 1; iCps++) + { + push_16bits(&payloadBinaryData, elm.gain_curve_control_points_y[iAlt][iCps], + "gain_curve_control_points_y[iAlt][iCps]", verboseLevel); + } + if(!elm.gain_curve_use_pchip_slope_flag[iAlt]) + { + for(uint16_t iCps = 0; iCps < elm.gain_curve_num_control_points_minus_1[iAlt] + 1; iCps++) + { + push_16bits(&payloadBinaryData, elm.gain_curve_control_points_theta[iAlt][iCps], + "gain_curve_control_points_theta[iAlt][iCps]", verboseLevel); + } + } + } + } + else + { // No more information need to be signaled when using Reference White Tone Mapping Operator + push_bits(&payloadBinaryData, 0, 7, "zero_7bits", verboseLevel); + } + } + // Verify binary is byte complete and popback last added new byte + if(payloadBinaryData.bitIdx != 0) + { + logMsg(LOGLEVEL_ERROR, "*Critical* Binary data writing did not finish with a full byte."); + } + else + { + payloadBinaryData.payload.pop_back(); + logMsg(LOGLEVEL_DEBUG, + "End SMPTE_ST2094_50::writeSyntaxElementsToBinaryData, payload size = %d bytes", + payloadBinaryData.byteIdx); + dbgPrintMetadataItems(); // Put here for easy comparison of logs + } +} + +/* *********************************** DECODING SECTION + * ********************************************************************************************/ + +// Decode binary data into syntax elements +void SMPTE_ST2094_50::decodeBinaryToSyntaxElements(std::vector binary_data) +{ + + // Adapt binary data + // Initialize the binary payload structure + payloadBinaryData.byteIdx = 0; + payloadBinaryData.bitIdx = 0; + for(int i = 0; i < int(binary_data.size()); i++) + { + payloadBinaryData.payload.push_back(binary_data[i]); + } + printBinaryData(binary_data, verboseLevel); + + logMsg(LOGLEVEL_DEBUG, "Syntax Elements Decoding"); + elm.application_version = pull_bits(&payloadBinaryData, 3, "application_version", verboseLevel); + elm.minimum_application_version = + pull_bits(&payloadBinaryData, 3, "minimum_application_version", verboseLevel); + pull_bits(&payloadBinaryData, 2, "zero_2bits", verboseLevel); + + elm.has_custom_hdr_reference_white_flag = + pull_boolean(&payloadBinaryData, "has_custom_hdr_reference_white_flag", verboseLevel); + elm.has_adaptive_tone_map_flag = + pull_boolean(&payloadBinaryData, "has_adaptive_tone_map_flag", verboseLevel); + pull_bits(&payloadBinaryData, 6, "zero_6bits", verboseLevel); + + if(elm.has_custom_hdr_reference_white_flag) + { + elm.hdr_reference_white = pull_16bits(&payloadBinaryData, "hdr_reference_white", verboseLevel); + } + + if(elm.has_adaptive_tone_map_flag) + { + elm.baseline_hdr_headroom = + pull_16bits(&payloadBinaryData, "baseline_hdr_headroom", verboseLevel); + + elm.use_reference_white_tone_mapping_flag = + pull_boolean(&payloadBinaryData, "use_reference_white_tone_mapping_flag", verboseLevel); + if(!elm.use_reference_white_tone_mapping_flag) + { + elm.num_alternate_images = + pull_bits(&payloadBinaryData, 3, "num_alternate_images", verboseLevel); + elm.gain_application_space_chromaticities_mode = pull_bits( + &payloadBinaryData, 2, "gain_application_space_chromaticities_mode", verboseLevel); + elm.has_common_component_mix_params_flag = + pull_boolean(&payloadBinaryData, "has_common_component_mix_params_flag", verboseLevel); + elm.has_common_curve_params_flag = + pull_boolean(&payloadBinaryData, "has_common_curve_params_flag", verboseLevel); + + if(elm.gain_application_space_chromaticities_mode == 3) + { + for(uint16_t iCh = 0; iCh < 8; iCh++) + { + elm.gain_application_space_chromaticities[iCh] = pull_16bits( + &payloadBinaryData, "gain_application_space_chromaticities[iCh]", verboseLevel); + } + } + + for(uint16_t iAlt = 0; iAlt < elm.num_alternate_images; iAlt++) + { + elm.alternate_hdr_headrooms[iAlt] = + pull_16bits(&payloadBinaryData, "alternate_hdr_headrooms[iAlt]", verboseLevel); + + // Read component mixing function parameters - Table C.4 + if(iAlt == 0 || !elm.has_common_component_mix_params_flag) + { + elm.component_mixing_type[iAlt] = + pull_bits(&payloadBinaryData, 2, "component_mixing_type[iAlt]", verboseLevel); + if(elm.component_mixing_type[iAlt] == 3) + { + uint8_t has_component_mixing_coefficient_flag = pull_bits( + &payloadBinaryData, 6, "has_component_mixing_coefficient_flag[iAlt]", verboseLevel); + // Decode the flags and the associated values + for(uint8_t iCm = 0; iCm < MAX_NB_COMPONENT_MIXING_COEFFICIENT; iCm++) + { + elm.has_component_mixing_coefficient_flag[iAlt][iCm] = + bool(has_component_mixing_coefficient_flag & (0x01 << (5 - iCm))); + if(elm.has_component_mixing_coefficient_flag[iAlt][iCm]) + { + elm.component_mixing_coefficient[iAlt][iCm] = pull_16bits( + &payloadBinaryData, "component_mixing_coefficient[iAlt][iCm]", verboseLevel); + } + else + { + elm.component_mixing_coefficient[iAlt][iCm] = 0; + } + } + } + else + { + pull_bits(&payloadBinaryData, 6, "zero_6bits[iAlt][iCm]", verboseLevel); + } + } + else + { + elm.component_mixing_type[iAlt] = elm.component_mixing_type[0]; + if(elm.component_mixing_type[0] == 3) + { + elm.component_mixing_type[iAlt] = elm.component_mixing_type[0]; + for(uint8_t iCm = 0; iCm < MAX_NB_COMPONENT_MIXING_COEFFICIENT; iCm++) + { + elm.has_component_mixing_coefficient_flag[iAlt][iCm] = + elm.has_component_mixing_coefficient_flag[iAlt][iCm]; + elm.component_mixing_coefficient[iAlt][iCm] = + elm.component_mixing_coefficient[0][iCm]; + } + } + } + + // Read gain curve function parameters - table C.5 + if(iAlt == 0 || !elm.has_common_curve_params_flag) + { + elm.gain_curve_num_control_points_minus_1[iAlt] = pull_bits( + &payloadBinaryData, 5, "gain_curve_num_control_points_minus_1[iAlt]", verboseLevel); + elm.gain_curve_use_pchip_slope_flag[iAlt] = + pull_boolean(&payloadBinaryData, "gain_curve_use_pchip_slope_flag[iAlt]", verboseLevel); + pull_bits(&payloadBinaryData, 2, "zero_2bits[iAlt]", verboseLevel); + + for(uint16_t iCps = 0; iCps < elm.gain_curve_num_control_points_minus_1[iAlt] + 1; iCps++) + { + elm.gain_curve_control_points_x[iAlt][iCps] = pull_16bits( + &payloadBinaryData, "gain_curve_control_points_x[iAlt][iCps]", verboseLevel); + } + } + else + { + elm.gain_curve_num_control_points_minus_1[iAlt] = + elm.gain_curve_num_control_points_minus_1[0]; + elm.gain_curve_use_pchip_slope_flag[iAlt] = elm.gain_curve_use_pchip_slope_flag[0]; + for(uint16_t iCps = 0; iCps < elm.gain_curve_num_control_points_minus_1[iAlt] + 1; iCps++) + { + elm.gain_curve_control_points_x[iAlt][iCps] = elm.gain_curve_control_points_x[0][iCps]; + } + } + for(uint16_t iCps = 0; iCps < elm.gain_curve_num_control_points_minus_1[iAlt] + 1; iCps++) + { + elm.gain_curve_control_points_y[iAlt][iCps] = pull_16bits( + &payloadBinaryData, "gain_curve_control_points_y[iAlt][iCps]", verboseLevel); + } + if(!elm.gain_curve_use_pchip_slope_flag[iAlt]) + { + for(uint16_t iCps = 0; iCps < elm.gain_curve_num_control_points_minus_1[iAlt] + 1; iCps++) + { + elm.gain_curve_control_points_theta[iAlt][iCps] = pull_16bits( + &payloadBinaryData, "gain_curve_control_points_theta[iAlt][iCps]", verboseLevel); + } + } + } + } + else + { + pull_bits(&payloadBinaryData, 7, "zero_7bits", verboseLevel); + } + } + logMsg(LOGLEVEL_DEBUG, "Syntax Elements Successfully Decoded"); +} + +// Convert the syntax elements to Metadata Items as described in Clause C.3 +void SMPTE_ST2094_50::convertSyntaxElementsToMetadataItems() +{ + + // get mandatory metadata + if(elm.has_custom_hdr_reference_white_flag) + { + cvt.hdrReferenceWhite = float(elm.hdr_reference_white) / Q_HDR_REFERENCE_WHITE; + } + else + { + cvt.hdrReferenceWhite = 203.0; + } + + // Get Optional metadata items + if(elm.has_adaptive_tone_map_flag) + { + isHeadroomAdaptiveToneMap = true; + HeadroomAdaptiveToneMap hatm; + cvt.hatm.baselineHdrHeadroom = float(elm.baseline_hdr_headroom) / Q_HDR_HEADROOM; + if(elm.use_reference_white_tone_mapping_flag) + { + isReferenceWhiteToneMapping = true; + cvt.hatm.numAlternateImages = 2; + // BT.2020 primaries + cvt.hatm.gainApplicationSpaceChromaticities[0] = 0.708; + cvt.hatm.gainApplicationSpaceChromaticities[1] = 0.292; + cvt.hatm.gainApplicationSpaceChromaticities[2] = 0.17; + cvt.hatm.gainApplicationSpaceChromaticities[3] = 0.797; + cvt.hatm.gainApplicationSpaceChromaticities[4] = 0.131; + cvt.hatm.gainApplicationSpaceChromaticities[5] = 0.046; + cvt.hatm.gainApplicationSpaceChromaticities[6] = 0.3127; + cvt.hatm.gainApplicationSpaceChromaticities[7] = 0.3290; + // Compute alternate headroom + cvt.hatm.alternateHdrHeadroom.push_back(0.0); + float headroom_to_anchor_ratio = + std::min(cvt.hatm.baselineHdrHeadroom / log2(1000.0 / 203.0), 1.0); + float h_alt_1 = log2(8.0 / 3.0) * headroom_to_anchor_ratio; + cvt.hatm.alternateHdrHeadroom.push_back(h_alt_1); + + // Constant parameter across alternate images + float kappa = 0.65; + float x_knee = 1; + float x_max = pow(2.0, cvt.hatm.baselineHdrHeadroom); + for(uint16_t iAlt = 0; iAlt < cvt.hatm.numAlternateImages; iAlt++) + { + hasSlopeParameter[iAlt] = true; + // Component mixing is maxRGB + ColorGainFunction cgf; + cgf.cm.componentMixRed = 0.0; + cgf.cm.componentMixGreen = 0.0; + cgf.cm.componentMixBlue = 0.0; + cgf.cm.componentMixMax = 1.0; + cgf.cm.componentMixMin = 0.0; + cgf.cm.componentMixComponent = 0.0; + cgf.gc.gainCurveNumControlPoints = 8; + + // Inner vector for push_back + std::vector inner_gainCurveControlPointX; + std::vector inner_gainCurveControlPointY; + std::vector inner_gainCurveControlPointM; + + // Compute the control points parameter depending on the alternate headroom + float y_white = 1.0; + if(iAlt == 0) + { + y_white = 1 - (0.5 * headroom_to_anchor_ratio); + } + float y_knee = y_white; + float y_max = pow(2.0, cvt.hatm.alternateHdrHeadroom[iAlt]); + float x_mid = (1.0 - kappa) * x_knee + kappa * (x_knee * y_max / y_knee); + float y_mid = (1.0 - kappa) * y_knee + kappa * y_max; + // Compute Quadratic Beziers coefficients + float a_x = x_knee - 2 * x_mid + x_max; + float a_y = y_knee - 2 * y_mid + y_max; + float b_x = 2 * x_mid - 2 * x_knee; + float b_y = 2 * y_mid - 2 * y_knee; + float c_x = x_knee; + float c_y = y_knee; + + for(uint16_t iCps = 0; iCps < cgf.gc.gainCurveNumControlPoints; iCps++) + { + // Compute the control points + float t = float(iCps) / (float(cgf.gc.gainCurveNumControlPoints) - 1.0); + float t_square = t * t; + float x = a_x * t_square + b_x * t + c_x; + float y = a_y * t_square + b_y * t + c_y; + float m = (2.0 * a_y * t + b_y) / (2 * a_x * t + b_x); + float slope = atan((x * m - y) / (log(2) * x * y)); + cgf.gc.gainCurveControlPointX.push_back(x); + cgf.gc.gainCurveControlPointY.push_back(log2(y / x)); + cgf.gc.gainCurveControlPointM.push_back(slope / PI_CUSTOM * 180.0); + } + cvt.hatm.cgf.push_back(cgf); + } + } + else + { + cvt.hatm.numAlternateImages = elm.num_alternate_images; + if(elm.gain_application_space_chromaticities_mode == 0) + { + cvt.hatm.gainApplicationSpaceChromaticities[0] = 0.64; + cvt.hatm.gainApplicationSpaceChromaticities[1] = 0.33; + cvt.hatm.gainApplicationSpaceChromaticities[2] = 0.3; + cvt.hatm.gainApplicationSpaceChromaticities[3] = 0.6; + cvt.hatm.gainApplicationSpaceChromaticities[4] = 0.15; + cvt.hatm.gainApplicationSpaceChromaticities[5] = 0.06; + cvt.hatm.gainApplicationSpaceChromaticities[6] = 0.3127; + cvt.hatm.gainApplicationSpaceChromaticities[7] = 0.329; + } + else if(elm.gain_application_space_chromaticities_mode == 1) + { + cvt.hatm.gainApplicationSpaceChromaticities[0] = 0.68; + cvt.hatm.gainApplicationSpaceChromaticities[1] = 0.32; + cvt.hatm.gainApplicationSpaceChromaticities[2] = 0.265; + cvt.hatm.gainApplicationSpaceChromaticities[3] = 0.69; + cvt.hatm.gainApplicationSpaceChromaticities[4] = 0.15; + cvt.hatm.gainApplicationSpaceChromaticities[5] = 0.06; + cvt.hatm.gainApplicationSpaceChromaticities[6] = 0.3127; + cvt.hatm.gainApplicationSpaceChromaticities[7] = 0.329; + } + else if(elm.gain_application_space_chromaticities_mode == 2) + { + cvt.hatm.gainApplicationSpaceChromaticities[0] = 0.708; + cvt.hatm.gainApplicationSpaceChromaticities[1] = 0.292; + cvt.hatm.gainApplicationSpaceChromaticities[2] = 0.17; + cvt.hatm.gainApplicationSpaceChromaticities[3] = 0.797; + cvt.hatm.gainApplicationSpaceChromaticities[4] = 0.131; + cvt.hatm.gainApplicationSpaceChromaticities[5] = 0.046; + cvt.hatm.gainApplicationSpaceChromaticities[6] = 0.3127; + cvt.hatm.gainApplicationSpaceChromaticities[7] = 0.329; + } + else if(elm.gain_application_space_chromaticities_mode == 3) + { + for(uint16_t iCh = 0; iCh < 8; iCh++) + { + cvt.hatm.gainApplicationSpaceChromaticities[iCh] = + float(elm.gain_application_space_chromaticities[iCh]) / + Q_GAIN_APPLICATION_SPACE_CHROMATICITY; + } + } + else + { + logMsg(LOGLEVEL_ERROR, "gain_application_space_primaries=%d not defined", + elm.gain_application_space_chromaticities_mode); + } + for(uint16_t iAlt = 0; iAlt < cvt.hatm.numAlternateImages; iAlt++) + { + cvt.hatm.alternateHdrHeadroom.push_back(float(elm.alternate_hdr_headrooms[iAlt]) / + Q_HDR_HEADROOM); + // init k_params to zero and replace the one that are not + ColorGainFunction cgf; + cgf.cm.componentMixRed = 0.0; + cgf.cm.componentMixGreen = 0.0; + cgf.cm.componentMixBlue = 0.0; + cgf.cm.componentMixMax = 0.0; + cgf.cm.componentMixMin = 0.0; + cgf.cm.componentMixComponent = 0.0; + if(elm.component_mixing_type[iAlt] == 0) + { + cgf.cm.componentMixMax = 1.0; + } + else if(elm.component_mixing_type[iAlt] == 1) + { + cgf.cm.componentMixComponent = 1.0; + } + else if(elm.component_mixing_type[iAlt] == 2) + { + cgf.cm.componentMixRed = 1.0 / 6.0; + cgf.cm.componentMixGreen = 1.0 / 6.0; + cgf.cm.componentMixBlue = 1.0 / 6.0; + cgf.cm.componentMixMax = 1.0 / 2.0; + } + else if(elm.component_mixing_type[iAlt] == 3) + { + cgf.cm.componentMixRed = 0.0f; + cgf.cm.componentMixGreen = 0.0f; + cgf.cm.componentMixBlue = 0.0f; + cgf.cm.componentMixMax = 0.0f; + cgf.cm.componentMixMin = 0.0f; + cgf.cm.componentMixComponent = 0.0f; + if(elm.component_mixing_type[iAlt] == 0) + { + cgf.cm.componentMixMax = 1.0f; + } + else if(elm.component_mixing_type[iAlt] == 1) + { + cgf.cm.componentMixComponent = 1.0f; + } + else if(elm.component_mixing_type[iAlt] == 2) + { + cgf.cm.componentMixMax = 1.0f / 2.0f; + cgf.cm.componentMixRed = 1.0f / 6.0f; + cgf.cm.componentMixGreen = 1.0f / 6.0f; + cgf.cm.componentMixBlue = 1.0f / 6.0f; + } + else if(elm.component_mixing_type[iAlt] == 3) + { + // Compute sum of component + float sumComponent = 0.0f; + for(int k = 0; k < MAX_NB_COMPONENT_MIXING_COEFFICIENT; k++) + { + sumComponent = float(elm.component_mixing_coefficient[iAlt][k]); + } + if(sumComponent != Q_COMPONENT_MIXING_COEFFICIENT) + { + logMsg(LOGLEVEL_WARNING, + "Sum component mixing coefficient for alternate %d is not equal to 1.0, they " + "will be scaled to 1.0.", + iAlt); + } + for(int k = 0; k < MAX_NB_COMPONENT_MIXING_COEFFICIENT; k++) + { + float value = 0.0f; + if(elm.has_component_mixing_coefficient_flag[iAlt][k]) + { + value = float(elm.component_mixing_coefficient[iAlt][k]) / sumComponent; + } + switch(k) + { + case 0: + cgf.cm.componentMixRed = value; + break; + case 1: + cgf.cm.componentMixGreen = value; + break; + case 2: + cgf.cm.componentMixBlue = value; + break; + case 3: + cgf.cm.componentMixMax = value; + break; + case 4: + cgf.cm.componentMixMin = value; + break; + case 5: + cgf.cm.componentMixComponent = value; + break; + } + } + } + } + else + { + logMsg(LOGLEVEL_ERROR, "mix_encoding[%d]=%d not defined", iAlt, + elm.component_mixing_type[iAlt]); + } + cgf.gc.gainCurveNumControlPoints = elm.gain_curve_num_control_points_minus_1[iAlt] + 1; + // Determine the sign of the gain coefficients based on headrooms difference + float sign = 1.0; + if(cvt.hatm.baselineHdrHeadroom > cvt.hatm.alternateHdrHeadroom[iAlt]) + { + sign = -1.0; + } + for(uint16_t iCps = 0; iCps < cgf.gc.gainCurveNumControlPoints; iCps++) + { + cgf.gc.gainCurveControlPointX.push_back( + float(elm.gain_curve_control_points_x[iAlt][iCps]) / Q_GAIN_CURVE_CONTROL_POINT_X); + cgf.gc.gainCurveControlPointY.push_back( + sign * float(elm.gain_curve_control_points_y[iAlt][iCps]) / + Q_GAIN_CURVE_CONTROL_POINT_Y); + if(!elm.gain_curve_use_pchip_slope_flag[iAlt]) + { + hasSlopeParameter[iAlt] = true; + float theta = float(elm.gain_curve_control_points_theta[iAlt][iCps]) / + Q_GAIN_CURVE_CONTROL_POINT_THETA - + O_GAIN_CURVE_CONTROL_POINT_THETA; + cgf.gc.gainCurveControlPointM.push_back(tan(theta * M_PI / 180.0f)); + } + } + cvt.hatm.cgf.push_back(cgf); + } + } + } +} + +nlohmann::json SMPTE_ST2094_50::encodeMetadataItemsToJson() +{ + nlohmann::json j; + + j["frame_start"] = timeI.timeIntervalStart; + j["frame_duration"] = timeI.timeintervalDuration; + + j["hdrReferenceWhite"] = cvt.hdrReferenceWhite; + if(isHeadroomAdaptiveToneMap) + { + j["baselineHdrHeadroom"] = cvt.hatm.baselineHdrHeadroom; + if(!isReferenceWhiteToneMapping) + { + j["numAlternateImages"] = cvt.hatm.numAlternateImages; + j["gainApplicationSpaceChromaticities"] = cvt.hatm.gainApplicationSpaceChromaticities; + + if(cvt.hatm.numAlternateImages > 0) + { + j["alternateHdrHeadroom"] = cvt.hatm.alternateHdrHeadroom; + + std::vector componentMixRed; + std::vector componentMixGreen; + std::vector componentMixBlue; + std::vector componentMixMax; + std::vector componentMixMin; + std::vector componentMixComponent; + + std::vector gainCurveNumControlPoints; + std::vector> gainCurveControlPointX_ptr; + std::vector> gainCurveControlPointY_ptr; + std::vector> gainCurveControlPointT_ptr; + // "Unnesting" the variables: Accessing individual members of each struct + for(size_t i = 0; i < cvt.hatm.cgf.size(); i++) + { // Range-based for loop for convenience + componentMixRed.push_back(cvt.hatm.cgf[i].cm.componentMixRed); + componentMixGreen.push_back(cvt.hatm.cgf[i].cm.componentMixGreen); + componentMixBlue.push_back(cvt.hatm.cgf[i].cm.componentMixBlue); + componentMixMax.push_back(cvt.hatm.cgf[i].cm.componentMixMax); + componentMixMin.push_back(cvt.hatm.cgf[i].cm.componentMixMin); + componentMixComponent.push_back(cvt.hatm.cgf[i].cm.componentMixComponent); + + gainCurveNumControlPoints.push_back(cvt.hatm.cgf[i].gc.gainCurveNumControlPoints); + std::vector gainCurveControlPointX; + std::vector gainCurveControlPointY; + std::vector gainCurveControlPointT; + for(uint32_t p = 0; p < cvt.hatm.cgf[i].gc.gainCurveNumControlPoints; p++) + { + gainCurveControlPointX.push_back(cvt.hatm.cgf[i].gc.gainCurveControlPointX[p]); + gainCurveControlPointY.push_back(cvt.hatm.cgf[i].gc.gainCurveControlPointY[p]); + } + for(size_t p = 0; p < cvt.hatm.cgf[i].gc.gainCurveControlPointM.size(); p++) + { + gainCurveControlPointT.push_back(cvt.hatm.cgf[i].gc.gainCurveControlPointM[p]); + } + gainCurveControlPointX_ptr.push_back(gainCurveControlPointX); + gainCurveControlPointY_ptr.push_back(gainCurveControlPointY); + gainCurveControlPointT_ptr.push_back(gainCurveControlPointT); + } + + j["componentMixRed"] = componentMixRed; + j["componentMixGreen"] = componentMixGreen; + j["componentMixBlue"] = componentMixBlue; + j["componentMixMax"] = componentMixMax; + j["componentMixMin"] = componentMixMin; + j["componentMixComponent"] = componentMixComponent; + + j["gainCurveNumControlPoints"] = gainCurveNumControlPoints; + j["gainCurveControlPointX"] = gainCurveControlPointX_ptr; + j["gainCurveControlPointY"] = gainCurveControlPointY_ptr; + j["gainCurveControlPointX"] = gainCurveControlPointT_ptr; + } + } + } + return j; +} + +/* *********************************** DEBUGGING SECTION + * *******************************************************************************************/ +// Print the metadata item +void SMPTE_ST2094_50::dbgPrintMetadataItems() +{ + // Only print at DEBUG level or higher + if(verboseLevel < LOGLEVEL_DEBUG) + { + return; + } + + logMsg(LOGLEVEL_DEBUG, "Start SMPTE_ST2094_50::dbgPrintMetadataItems"); + std::cout << "windowNumber=" << pWin.windowNumber << "\n"; + std::cout << "hdrReferenceWhite=" << cvt.hdrReferenceWhite << "\n"; + if(isHeadroomAdaptiveToneMap) + { + std::cout << "baselineHdrHeadroom=" << cvt.hatm.baselineHdrHeadroom << "\n"; + if(!isReferenceWhiteToneMapping) + { + std::cout << "numAlternateImages=" << cvt.hatm.numAlternateImages << "\n"; + + std::cout << "gainApplicationSpaceChromaticities=["; + for(float val : cvt.hatm.gainApplicationSpaceChromaticities) + { + std::cout << val << ", "; + } + std::cout << "]" << std::endl; + + for(uint32_t iAlt = 0; iAlt < cvt.hatm.numAlternateImages; iAlt++) + { + std::cout << "alternateHdrHeadroom=" << cvt.hatm.alternateHdrHeadroom[iAlt] << "\n"; + std::cout << "componentMixRed=" << cvt.hatm.cgf[iAlt].cm.componentMixRed << "\n"; + std::cout << "componentMixGreen=" << cvt.hatm.cgf[iAlt].cm.componentMixGreen << "\n"; + std::cout << "componentMixBlue=" << cvt.hatm.cgf[iAlt].cm.componentMixBlue << "\n"; + std::cout << "componentMixMax=" << cvt.hatm.cgf[iAlt].cm.componentMixMax << "\n"; + std::cout << "componentMixMin=" << cvt.hatm.cgf[iAlt].cm.componentMixMin << "\n"; + std::cout << "componentMixComponent=" << cvt.hatm.cgf[iAlt].cm.componentMixComponent + << "\n"; + + std::cout << "gainCurveNumControlPoints=" << cvt.hatm.cgf[iAlt].gc.gainCurveNumControlPoints + << "\n"; + + std::cout << "gainCurveControlPointX=[" << std::endl; + for(float val : cvt.hatm.cgf[iAlt].gc.gainCurveControlPointX) + { + std::cout << val << ", "; + } + std::cout << "]" << std::endl; + + std::cout << "gainCurveControlPointY=[" << std::endl; + for(float val : cvt.hatm.cgf[iAlt].gc.gainCurveControlPointY) + { + std::cout << val << ", "; + } + std::cout << "]" << std::endl; + + std::cout << "gainCurveControlPointM=[" << std::endl; + for(float val : cvt.hatm.cgf[iAlt].gc.gainCurveControlPointM) + { + std::cout << val << ", "; + } + std::cout << "]" << std::endl; + } + } + } + logMsg(LOGLEVEL_DEBUG, "End SMPTE_ST2094_50::dbgPrintMetadataItems"); +} diff --git a/IsoLib/t35_tool/sources/SMPTE_ST2094_50.hpp b/IsoLib/t35_tool/sources/SMPTE_ST2094_50.hpp new file mode 100644 index 00000000..2bd716ff --- /dev/null +++ b/IsoLib/t35_tool/sources/SMPTE_ST2094_50.hpp @@ -0,0 +1,164 @@ +#ifndef SMPTE_ST2094_50_HPP +#define SMPTE_ST2094_50_HPP +#include +#include +#include +#include + +// 3rd party headers +#include + +const float PI_CUSTOM = 3.14159265358979323846; + +const int MAX_NB_ALTERNATE = 4; +const int MAX_NB_CONTROL_POINTS = 32; +const int MAX_NB_CHROMATICITIES = 8; +const int MAX_NB_COMPONENT_MIXING_COEFFICIENT = 6; + +// Compute quantization error of each float to uint16_t +const float Q_HDR_REFERENCE_WHITE = 5.0f; // Scaling 0.2 to 10000.0 range to 1-50000 (sampling of 0.2) +const float Q_HDR_HEADROOM = 10000.0f; // Scaling 0.0 to +6.0 range to 0-60000 (sampling of 0.00001) +const float P_HDR_HEADROOM = 0.5f / Q_HDR_HEADROOM; // Scaling 0.0 to +6.0 range to 0-60000 (sampling of 0.00001) +const float Q_GAIN_APPLICATION_SPACE_CHROMATICITY = 50000.0f; // Scaling 0.0 to +1.0 range to 0-50000 (sampling of 0.00002) +const float P_GAIN_APPLICATION_SPACE_CHROMATICITY = 0.5f /Q_GAIN_APPLICATION_SPACE_CHROMATICITY; // Maximum quantization error of chromaticity +const float Q_COMPONENT_MIXING_COEFFICIENT = 50000.0f; // Scaling 0.0 to +1.0 range to 0-50000 (sampling of 0.00002) +const float P_COMPONENT_MIXING_COEFFICIENT = 0.5f / Q_COMPONENT_MIXING_COEFFICIENT; // Maximum quantization error of component mixing coefficient +const float Q_GAIN_CURVE_CONTROL_POINT_X = 1000.0f; // Scaling 0.0 to +64.0 range to 0-64000 (sampling of 0.0001) +const float Q_GAIN_CURVE_CONTROL_POINT_Y = 10000.0f; // Scaling 0.0 to +6.0 range to 0-60000 (sampling of 0.00001) +const float O_GAIN_CURVE_CONTROL_POINT_THETA = 90.0f; // Offset to bring -90.0 to +90.0 range to 0-180 +const float Q_GAIN_CURVE_CONTROL_POINT_THETA = 200.0f; // Scaling -90.0 to +90.0 range to 0-36000 (sampling of 0.005) + +struct BinaryData{ + std::vector payload; + uint16_t byteIdx; + uint8_t bitIdx; + }; + +struct GainCurve{ + // std::vector gainCurveInterpolation; -> not in the spec currently + uint32_t gainCurveNumControlPoints; + std::vector gainCurveControlPointX; + std::vector gainCurveControlPointY; + std::vector gainCurveControlPointM; +}; + +struct ComponentMix{ + float componentMixRed; + float componentMixGreen; + float componentMixBlue; + float componentMixMax; + float componentMixMin; + float componentMixComponent; +}; + +struct ColorGainFunction{ + ComponentMix cm; + GainCurve gc; +}; + +struct HeadroomAdaptiveToneMap{ + float baselineHdrHeadroom; + + uint32_t numAlternateImages; + float gainApplicationSpaceChromaticities[MAX_NB_CHROMATICITIES]; + std::vector alternateHdrHeadroom; + std::vector cgf; +}; + +struct ColorVolumeTransform{ + float hdrReferenceWhite; + HeadroomAdaptiveToneMap hatm; +}; + +struct ProcessingWindow{ + uint32_t upperLeftCorner; + uint32_t lowerRightCorner; + uint32_t windowNumber; +}; + +struct TimeInterval{ + uint32_t timeIntervalStart; + uint32_t timeintervalDuration; +}; + +// Structure with the syntax elements +struct SyntaxElements { + // smpte_st_2094_50_application_info + uint16_t application_version; + uint16_t minimum_application_version; + + // smpte_st_2094_50_color_volume_transform + bool has_custom_hdr_reference_white_flag; + bool has_adaptive_tone_map_flag; + uint16_t hdr_reference_white ; + + // smpte_st_2094_50_adaptive_tone_map + uint16_t baseline_hdr_headroom ; + bool use_reference_white_tone_mapping_flag; + uint16_t num_alternate_images; + uint16_t gain_application_space_chromaticities_mode; + bool has_common_component_mix_params_flag; + bool has_common_curve_params_flag; + uint16_t gain_application_space_chromaticities[MAX_NB_CHROMATICITIES]; + uint16_t alternate_hdr_headrooms[MAX_NB_ALTERNATE]; + + // smpte_st_2094_50_component_mixing + uint16_t component_mixing_type[MAX_NB_ALTERNATE]; + bool has_component_mixing_coefficient_flag[MAX_NB_ALTERNATE][MAX_NB_COMPONENT_MIXING_COEFFICIENT]; + uint16_t component_mixing_coefficient[MAX_NB_ALTERNATE][MAX_NB_COMPONENT_MIXING_COEFFICIENT]; + + // smpte_st_2094_50_gain_curve + uint16_t gain_curve_num_control_points_minus_1[MAX_NB_ALTERNATE]; + bool gain_curve_use_pchip_slope_flag[MAX_NB_ALTERNATE]; + uint16_t gain_curve_control_points_x[MAX_NB_ALTERNATE][MAX_NB_CONTROL_POINTS]; + uint16_t gain_curve_control_points_y[MAX_NB_ALTERNATE][MAX_NB_CONTROL_POINTS]; + uint16_t gain_curve_control_points_theta[MAX_NB_ALTERNATE][MAX_NB_CONTROL_POINTS]; + }; + +class SMPTE_ST2094_50 { +public: + SMPTE_ST2094_50(); // Constructor + bool decodeJsonToMetadataItems(nlohmann::json j); + void convertMetadataItemsToSyntaxElements(); + void writeSyntaxElementsToBinaryData(); + + void decodeBinaryToSyntaxElements(std::vector binary_data); + void convertSyntaxElementsToMetadataItems(); + nlohmann::json encodeMetadataItemsToJson(); + + // Getters + std::vector getPayloadData(); + uint32_t getTimeIntervalStart(); + uint32_t getTimeintervalDuration(); + int getVerboseLevel() const { return verboseLevel; } + + // Setters + void setTimeIntervalStart(uint32_t frame_start); + void setTimeintervalDuration(uint32_t frame_duration); + void setVerboseLevel(int level); + + + void dbgPrintMetadataItems(); + // Carrying mechanism information + std::string keyValue; + BinaryData payloadBinaryData; + +private: + uint8_t applicationIdentifier; + uint8_t applicationVersion; + TimeInterval timeI; + ProcessingWindow pWin; + ColorVolumeTransform cvt; + + // not in specification, convenience flag for implementation + bool isHeadroomAdaptiveToneMap; + bool isReferenceWhiteToneMapping; + bool hasSlopeParameter[MAX_NB_ALTERNATE]; + + SyntaxElements elm; + + // Verbose level control + int verboseLevel; +}; + +#endif // SMPTE_ST2094_50_HPP \ No newline at end of file diff --git a/IsoLib/t35_tool/t35_tool.cpp b/IsoLib/t35_tool/t35_tool.cpp new file mode 100644 index 00000000..1a200d9c --- /dev/null +++ b/IsoLib/t35_tool/t35_tool.cpp @@ -0,0 +1,331 @@ +/** + * @file t35_tool.cpp + * @brief T.35 Metadata Tool - New Architecture + * + * This is the new modular implementation with clean separation of concerns: + * - Sources (input formats) + * - Injection strategies (MP4 container methods) + * - Extraction strategies (output formats) + */ + +// Standard library +#include +#include + +// Third-party +#include + +// libisomediafile +extern "C" +{ +#include "MP4Movies.h" +} + +// T35 tool +#include "common/Logger.hpp" +#include "common/MetadataTypes.hpp" +#include "common/T35Prefix.hpp" +#include "sources/MetadataSource.hpp" +#include "injection/InjectionStrategy.hpp" +#include "extraction/ExtractionStrategy.hpp" + +using namespace t35; + +// ============================================================================ +// Helper Functions +// ============================================================================ + +void printVersion() +{ + std::cout << "t35_tool v2.0 - T.35 Metadata Tool (New Architecture)\n"; + std::cout << "Built: " << __DATE__ << " " << __TIME__ << "\n"; +} + +void printAvailableOptions() +{ + std::cout << "\n"; + std::cout << "Available source types:\n"; + std::cout << " json-manifest (or generic-json) - Simple JSON with binary file references\n"; + std::cout << " smpte-folder (or json-folder) - Folder with SMPTE ST2094-50 JSON files\n"; + std::cout << "\n"; + std::cout << "Available injection methods:\n"; + std::cout << " mebx-me4c - MEBX track with me4c namespace (default)\n"; + std::cout << " dedicated-it35 - Dedicated metadata track\n"; + std::cout << " sample-group - Sample group\n"; + std::cout << "\n"; + std::cout << "Available extraction methods:\n"; + std::cout << " auto - Auto-detect (default)\n"; + std::cout << " mebx-me4c - MEBX with me4c namespace\n"; + std::cout << " dedicated-it35 - Dedicated metadata track\n"; + std::cout << " sample-group - Sample group\n"; + std::cout << " sei - Convert to video with SEI (stub)\n"; + std::cout << "\n"; +} + +// ============================================================================ +// Inject Command +// ============================================================================ + +int doInject(const std::string &inputFile, const std::string &outputFile, + const std::string &sourceSpec, const std::string &methodName, + const std::string &prefixStr) +{ + + LOG_INFO("=== T.35 Metadata Injection ==="); + LOG_INFO("Input: {}", inputFile); + LOG_INFO("Output: {}", outputFile); + LOG_INFO("Source: {}", sourceSpec); + LOG_INFO("Method: {}", methodName); + LOG_INFO("Prefix: {}", prefixStr); + + try + { + // Parse T.35 prefix + T35Prefix prefix(prefixStr); + if(!prefix.isValid()) + { + LOG_ERROR("Invalid T.35 prefix: {}", prefixStr); + return 1; + } + LOG_INFO("T.35 Prefix: {} ({})", prefix.hex(), prefix.description()); + + // Create source + auto source = createMetadataSource(sourceSpec); + LOG_INFO("Created source: {} at {}", source->getType(), source->getPath()); + + // Validate source + std::string errorMsg; + if(!source->validate(errorMsg)) + { + LOG_ERROR("Source validation failed: {}", errorMsg); + return 1; + } + + // Load metadata + MetadataMap items = source->load(prefix); + + // Validate metadata + if(!validateMetadataMap(items, errorMsg)) + { + LOG_ERROR("Metadata validation failed: {}", errorMsg); + return 1; + } + + // Open input movie + LOG_INFO("Opening input movie..."); + MP4Movie movie = nullptr; + MP4Err err = MP4OpenMovieFile(&movie, inputFile.c_str(), MP4OpenMovieNormal); + if(err || !movie) + { + LOG_ERROR("Failed to open input movie: {} (err={})", inputFile, err); + return 1; + } + + // Create injection strategy + auto strategy = createInjectionStrategy(methodName); + LOG_INFO("Created injection strategy: {}", strategy->getName()); + + // Prepare injection config + InjectionConfig config; + config.movie = movie; + config.t35Prefix = prefix.hex(); + // TODO: Find video track and get sample durations + + // Check applicability + std::string reason; + if(!strategy->isApplicable(items, config, reason)) + { + LOG_ERROR("Strategy '{}' not applicable: {}", methodName, reason); + MP4DisposeMovie(movie); + return 1; + } + + // Inject + err = strategy->inject(config, items, prefix); + if(err) + { + LOG_ERROR("Injection failed with error: {}", err); + MP4DisposeMovie(movie); + return 1; + } + + // Write output + LOG_INFO("Writing output movie..."); + err = MP4WriteMovieToFile(movie, outputFile.c_str()); + if(err) + { + LOG_ERROR("Failed to write output movie: {}", err); + MP4DisposeMovie(movie); + return 1; + } + + MP4DisposeMovie(movie); + return 0; + } + catch(const T35Exception &e) + { + LOG_ERROR("T.35 Error: {}", e.what()); + return 1; + } + catch(const std::exception &e) + { + LOG_ERROR("Error: {}", e.what()); + return 1; + } +} + +// ============================================================================ +// Extract Command +// ============================================================================ + +int doExtract(const std::string &inputFile, const std::string &outputPath, + const std::string &methodName, const std::string &prefixStr) +{ + + LOG_INFO("=== T.35 Metadata Extraction ==="); + LOG_INFO("Input: {}", inputFile); + LOG_INFO("Output: {}", outputPath); + LOG_INFO("Method: {}", methodName); + LOG_INFO("Prefix: {}", prefixStr); + + try + { + // Parse T.35 prefix + T35Prefix prefix(prefixStr); + if(!prefix.isValid()) + { + LOG_ERROR("Invalid T.35 prefix: {}", prefixStr); + return 1; + } + LOG_INFO("T.35 Prefix: {} ({})", prefix.hex(), prefix.description()); + + // Open input movie + LOG_INFO("Opening input movie..."); + MP4Movie movie = nullptr; + MP4Err err = MP4OpenMovieFile(&movie, inputFile.c_str(), MP4OpenMovieNormal); + if(err || !movie) + { + LOG_ERROR("Failed to open input movie: {} (err={})", inputFile, err); + return 1; + } + + // Create extraction strategy + auto strategy = createExtractionStrategy(methodName); + LOG_INFO("Created extraction strategy: {}", strategy->getName()); + + // Prepare extraction config + ExtractionConfig config; + config.movie = movie; + config.outputPath = outputPath; + config.t35Prefix = prefix.toString(); // Use full string with description + + // Only validate for non-auto strategies (auto tries all strategies internally) + if(methodName != "auto") + { + std::string reason; + if(!strategy->canExtract(config, reason)) + { + LOG_ERROR("Cannot extract with '{}': {}", methodName, reason); + MP4DisposeMovie(movie); + return 1; + } + } + + // Extract + err = strategy->extract(config); + if(err) + { + LOG_ERROR("Extraction failed with error: {}", err); + MP4DisposeMovie(movie); + return 1; + } + + MP4DisposeMovie(movie); + return 0; + } + catch(const T35Exception &e) + { + LOG_ERROR("T.35 Error: {}", e.what()); + return 1; + } + catch(const std::exception &e) + { + LOG_ERROR("Error: {}", e.what()); + return 1; + } +} + +// ============================================================================ +// Main +// ============================================================================ + +int main(int argc, char **argv) +{ + CLI::App app{"T.35 Metadata Tool - Modular Architecture"}; + app.set_version_flag("--version,-v", "2.0"); + app.footer("Use --help with subcommands for more information"); + + // Global options + int verbose = 2; // 0=error, 1=warn, 2=info, 3=debug + app.add_option("--verbose", verbose, "Verbosity level (0-3)") + ->default_val(2) + ->check(CLI::Range(0, 3)); + + bool listOptions = false; + app.add_flag("--list-options", listOptions, "List available source types and methods"); + + // ========== INJECT SUBCOMMAND ========== + auto inject = app.add_subcommand("inject", "Inject metadata into MP4"); + + std::string injectInput, injectOutput, injectSource; + std::string injectMethod = "mebx-me4c"; + std::string injectPrefix = "B500900001:SMPTE-ST2094-50"; + + inject->add_option("input", injectInput, "Input MP4 file")->required(); + inject->add_option("output", injectOutput, "Output MP4 file")->required(); + inject->add_option("--source,-s", injectSource, "Source spec (type:path)")->required(); + inject->add_option("--method,-m", injectMethod, "Injection method")->default_val("mebx-me4c"); + inject->add_option("--t35-prefix,-p", injectPrefix, "T.35 prefix (hex[:description])") + ->default_val("B500900001:SMPTE-ST2094-50"); + + // ========== EXTRACT SUBCOMMAND ========== + auto extract = app.add_subcommand("extract", "Extract metadata from MP4"); + + std::string extractInput, extractOutput; + std::string extractMethod = "auto"; + std::string extractPrefix = "B500900001:SMPTE-ST2094-50"; + + extract->add_option("input", extractInput, "Input MP4 file")->required(); + extract->add_option("output", extractOutput, "Output directory or file")->required(); + extract->add_option("--method,-m", extractMethod, "Extraction method")->default_val("auto"); + extract->add_option("--t35-prefix,-p", extractPrefix, "T.35 prefix (hex[:description])") + ->default_val("B500900001:SMPTE-ST2094-50"); + + // ========== PARSE ========== + CLI11_PARSE(app, argc, argv); + + // Initialize logger + Logger::init(verbose); + + if(listOptions) + { + printVersion(); + printAvailableOptions(); + return 0; + } + + // Execute subcommand + if(*inject) + { + return doInject(injectInput, injectOutput, injectSource, injectMethod, injectPrefix); + } + else if(*extract) + { + return doExtract(extractInput, extractOutput, extractMethod, extractPrefix); + } + else + { + std::cout << app.help() << "\n"; + return 0; + } +} diff --git a/TestData/t35_tool/CustomTMO/0_ST2094-50_ClampInRec601_metadataItems.json b/TestData/t35_tool/CustomTMO/0_ST2094-50_ClampInRec601_metadataItems.json new file mode 100644 index 00000000..bcfc4024 --- /dev/null +++ b/TestData/t35_tool/CustomTMO/0_ST2094-50_ClampInRec601_metadataItems.json @@ -0,0 +1,25 @@ + { + "SMPTEST2094_50": { + "frameStart": 0, + "frameDuration": 1, + "windowNumber": 1, + "ColorVolumeTransform": { + "hdrReferenceWhite": 100, + "HeadroomAdaptiveToneMapping": { + "baselineHdrHeadroom": 2, + "numAlternateImages": 0, + "gainApplicationSpaceChromaticities": [ + 0.64, + 0.33, + 0.29, + 0.6, + 0.15, + 0.06, + 0.3127, + 0.329 + ], + "alternateHdrHeadroom": [] + } + } + } +} \ No newline at end of file diff --git a/TestData/t35_tool/CustomTMO/1_ST2094-50_OneAlternates_metadataItems.json b/TestData/t35_tool/CustomTMO/1_ST2094-50_OneAlternates_metadataItems.json new file mode 100644 index 00000000..a40c7c7d --- /dev/null +++ b/TestData/t35_tool/CustomTMO/1_ST2094-50_OneAlternates_metadataItems.json @@ -0,0 +1,50 @@ + { + "SMPTEST2094_50": { + "frameStart": 1, + "frameDuration": 1, + "windowNumber": 1, + "ColorVolumeTransform": { + "hdrReferenceWhite": 400, + "HeadroomAdaptiveToneMapping": { + "baselineHdrHeadroom": 4, + "numAlternateImages": 1, + "gainApplicationSpaceChromaticities": [ + 0.68, + 0.32, + 0.265, + 0.69, + 0.15, + 0.06, + 0.3127, + 0.329 + ], + "alternateHdrHeadroom": 0, + "ColorGainFunction": { + "ComponentMix": { + "componentMixRed": 0, + "componentMixGreen": 0, + "componentMixBlue": 0, + "componentMixMax": 1, + "componentMixMin": 0, + "componentMixComponent": 0 + }, + "GainCurve": { + "gainCurveNumControlPoints": 2, + "gainCurveControlPointX": [ + 1, + 16 + ], + "gainCurveControlPointY": [ + 0, + -4 + ], + "gainCurveControlPointM": [ + 0, + 0 + ] + } + } + } + } + } +} \ No newline at end of file diff --git a/TestData/t35_tool/CustomTMO/2_ST2094-50_OneAlternates-MaxValues_metadataItems.json b/TestData/t35_tool/CustomTMO/2_ST2094-50_OneAlternates-MaxValues_metadataItems.json new file mode 100644 index 00000000..3adca480 --- /dev/null +++ b/TestData/t35_tool/CustomTMO/2_ST2094-50_OneAlternates-MaxValues_metadataItems.json @@ -0,0 +1,41 @@ + { + "SMPTEST2094_50": { + "frameStart": 2, + "frameDuration": 1, + "windowNumber": 1, + "ColorVolumeTransform": { + "hdrReferenceWhite": 203, + "HeadroomAdaptiveToneMapping": { + "baselineHdrHeadroom": 6, + "numAlternateImages": 1, + "gainApplicationSpaceChromaticities": [ + 0.708, + 0.292, + 0.17, + 0.797, + 0.131, + 0.046, + 0.3127, + 0.329 + ], + "alternateHdrHeadroom": 0, + "ColorGainFunction": { + "ComponentMix": { + "componentMixRed": 0, + "componentMixGreen": 0, + "componentMixBlue": 0, + "componentMixMax": 1, + "componentMixMin": 0, + "componentMixComponent": 0 + }, + "GainCurve": { + "gainCurveNumControlPoints": 1, + "gainCurveControlPointX": 64, + "gainCurveControlPointY": -6, + "gainCurveControlPointM": 0 + } + } + } + } + } +} \ No newline at end of file diff --git a/TestData/t35_tool/CustomTMO/4_ST2094-50_FourAlternates_metadataItems.json b/TestData/t35_tool/CustomTMO/4_ST2094-50_FourAlternates_metadataItems.json new file mode 100644 index 00000000..bdbfeb51 --- /dev/null +++ b/TestData/t35_tool/CustomTMO/4_ST2094-50_FourAlternates_metadataItems.json @@ -0,0 +1,135 @@ + { + "SMPTEST2094_50": { + "frameStart": 3, + "frameDuration": 1, + "windowNumber": 1, + "ColorVolumeTransform": { + "hdrReferenceWhite": 400, + "HeadroomAdaptiveToneMapping": { + "baselineHdrHeadroom": 2, + "numAlternateImages": 4, + "gainApplicationSpaceChromaticities": [ + 0.68, + 0.32, + 0.265, + 0.69, + 0.15, + 0.06, + 0.3127, + 0.329 + ], + "alternateHdrHeadroom": [ + 0, + 1, + 3, + 4 + ], + "ColorGainFunction": [ + { + "ComponentMix": { + "componentMixRed": 0, + "componentMixGreen": 0, + "componentMixBlue": 0, + "componentMixMax": 0.75, + "componentMixMin": 0.25, + "componentMixComponent": 0 + }, + "GainCurve": { + "gainCurveNumControlPoints": 1, + "gainCurveControlPointX": 64, + "gainCurveControlPointY": -6, + "gainCurveControlPointM": 0 + } + }, + { + "ComponentMix": { + "componentMixRed": 0, + "componentMixGreen": 0, + "componentMixBlue": 0, + "componentMixMax": 1, + "componentMixMin": 0, + "componentMixComponent": 0 + }, + "GainCurve": { + "gainCurveNumControlPoints": 4, + "gainCurveControlPointX": [ + 0, + 1, + 2, + 3 + ], + "gainCurveControlPointY": [ + -1, + -0.5, + -0.4, + -0.3 + ], + "gainCurveControlPointM": [ + 0, + 0.1, + 0.2, + 0.3 + ] + } + }, + { + "ComponentMix": { + "componentMixRed": 0, + "componentMixGreen": 0, + "componentMixBlue": 0, + "componentMixMax": 0, + "componentMixMin": 0, + "componentMixComponent": 1 + }, + "GainCurve": { + "gainCurveNumControlPoints": 2, + "gainCurveControlPointX": [ + 0, + 1 + ], + "gainCurveControlPointY": [ + 1, + 0.5 + ], + "gainCurveControlPointM": [ + 0, + 0.1 + ] + } + }, + { + "ComponentMix": { + "componentMixRed": 0.3, + "componentMixGreen": 0.6, + "componentMixBlue": 0.1, + "componentMixMax": 0, + "componentMixMin": 0, + "componentMixComponent": 0 + }, + "GainCurve": { + "gainCurveNumControlPoints": 4, + "gainCurveControlPointX": [ + 0, + 1, + 2, + 2 + ], + "gainCurveControlPointY": [ + 1, + 0.5, + 0.4, + 0.4 + ], + "gainCurveControlPointM": [ + 0, + 0.1, + 0.5, + 0.7 + ] + } + } + ] + } + } + } +} \ No newline at end of file diff --git a/TestData/t35_tool/DefaultToneMapRWTMO/0_ST2094-50_RWTMO-min-headroom_metadataItems.json b/TestData/t35_tool/DefaultToneMapRWTMO/0_ST2094-50_RWTMO-min-headroom_metadataItems.json new file mode 100644 index 00000000..e51f4e50 --- /dev/null +++ b/TestData/t35_tool/DefaultToneMapRWTMO/0_ST2094-50_RWTMO-min-headroom_metadataItems.json @@ -0,0 +1,13 @@ + { + "SMPTEST2094_50": { + "frameStart": 0, + "frameDuration": 1, + "windowNumber": 1, + "ColorVolumeTransform": { + "hdrReferenceWhite": 203, + "HeadroomAdaptiveToneMapping": { + "baselineHdrHeadroom": 0 + } + } + } +} \ No newline at end of file diff --git a/TestData/t35_tool/DefaultToneMapRWTMO/1_ST2094-50_RWTMO-mid-headroom_metadataItems.json b/TestData/t35_tool/DefaultToneMapRWTMO/1_ST2094-50_RWTMO-mid-headroom_metadataItems.json new file mode 100644 index 00000000..455a4641 --- /dev/null +++ b/TestData/t35_tool/DefaultToneMapRWTMO/1_ST2094-50_RWTMO-mid-headroom_metadataItems.json @@ -0,0 +1,13 @@ + { + "SMPTEST2094_50": { + "frameStart": 1, + "frameDuration": 1, + "windowNumber": 1, + "ColorVolumeTransform": { + "hdrReferenceWhite": 203, + "HeadroomAdaptiveToneMapping": { + "baselineHdrHeadroom": 3 + } + } + } +} \ No newline at end of file diff --git a/TestData/t35_tool/DefaultToneMapRWTMO/2_ST2094-50_RWTMO-max-headroom_metadataItems.json b/TestData/t35_tool/DefaultToneMapRWTMO/2_ST2094-50_RWTMO-max-headroom_metadataItems.json new file mode 100644 index 00000000..bbae2310 --- /dev/null +++ b/TestData/t35_tool/DefaultToneMapRWTMO/2_ST2094-50_RWTMO-max-headroom_metadataItems.json @@ -0,0 +1,13 @@ + { + "SMPTEST2094_50": { + "frameStart": 2, + "frameDuration": 1, + "windowNumber": 1, + "ColorVolumeTransform": { + "hdrReferenceWhite": 203, + "HeadroomAdaptiveToneMapping": { + "baselineHdrHeadroom": 6 + } + } + } +} \ No newline at end of file diff --git a/TestData/t35_tool/NoAdaptiveToneMap/0_ST2094-50_NoAdaptiveToneMap-DefaultWhite_metadataItems.json b/TestData/t35_tool/NoAdaptiveToneMap/0_ST2094-50_NoAdaptiveToneMap-DefaultWhite_metadataItems.json new file mode 100644 index 00000000..a4807c87 --- /dev/null +++ b/TestData/t35_tool/NoAdaptiveToneMap/0_ST2094-50_NoAdaptiveToneMap-DefaultWhite_metadataItems.json @@ -0,0 +1,10 @@ + { + "SMPTEST2094_50": { + "frameStart": 0, + "frameDuration": 1, + "windowNumber": 1, + "ColorVolumeTransform": { + "hdrReferenceWhite": 203 + } + } +} \ No newline at end of file diff --git a/TestData/t35_tool/NoAdaptiveToneMap/1_ST2094-50_NoAdaptiveToneMap-123-White_metadataItems.json b/TestData/t35_tool/NoAdaptiveToneMap/1_ST2094-50_NoAdaptiveToneMap-123-White_metadataItems.json new file mode 100644 index 00000000..11801086 --- /dev/null +++ b/TestData/t35_tool/NoAdaptiveToneMap/1_ST2094-50_NoAdaptiveToneMap-123-White_metadataItems.json @@ -0,0 +1,10 @@ + { + "SMPTEST2094_50": { + "frameStart": 1, + "frameDuration": 1, + "windowNumber": 1, + "ColorVolumeTransform": { + "hdrReferenceWhite": 123 + } + } +} \ No newline at end of file diff --git a/TestData/t35_tool/NoAdaptiveToneMap/2_ST2094-50_NoAdaptiveToneMap-MinWhite_metadataItems.json b/TestData/t35_tool/NoAdaptiveToneMap/2_ST2094-50_NoAdaptiveToneMap-MinWhite_metadataItems.json new file mode 100644 index 00000000..29fa7af6 --- /dev/null +++ b/TestData/t35_tool/NoAdaptiveToneMap/2_ST2094-50_NoAdaptiveToneMap-MinWhite_metadataItems.json @@ -0,0 +1,10 @@ + { + "SMPTEST2094_50": { + "frameStart": 2, + "frameDuration": 1, + "windowNumber": 1, + "ColorVolumeTransform": { + "hdrReferenceWhite": 0.2 + } + } +} \ No newline at end of file diff --git a/TestData/t35_tool/NoAdaptiveToneMap/3_ST2094-50_NoAdaptiveToneMap-MaxWhite_metadataItems.json b/TestData/t35_tool/NoAdaptiveToneMap/3_ST2094-50_NoAdaptiveToneMap-MaxWhite_metadataItems.json new file mode 100644 index 00000000..9e3a7824 --- /dev/null +++ b/TestData/t35_tool/NoAdaptiveToneMap/3_ST2094-50_NoAdaptiveToneMap-MaxWhite_metadataItems.json @@ -0,0 +1,10 @@ + { + "SMPTEST2094_50": { + "frameStart": 3, + "frameDuration": 1, + "windowNumber": 1, + "ColorVolumeTransform": { + "hdrReferenceWhite": 10000 + } + } +} \ No newline at end of file diff --git a/TestData/t35_tool/ST2094-50_LightDetector.mov b/TestData/t35_tool/ST2094-50_LightDetector.mov new file mode 100644 index 00000000..43b9eabe Binary files /dev/null and b/TestData/t35_tool/ST2094-50_LightDetector.mov differ diff --git a/TestData/t35_tool/meta_001.bin b/TestData/t35_tool/meta_001.bin new file mode 100644 index 00000000..b66efb8a Binary files /dev/null and b/TestData/t35_tool/meta_001.bin differ diff --git a/TestData/t35_tool/meta_002.bin b/TestData/t35_tool/meta_002.bin new file mode 100644 index 00000000..b18aeab5 --- /dev/null +++ b/TestData/t35_tool/meta_002.bin @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/TestData/t35_tool/meta_003.bin b/TestData/t35_tool/meta_003.bin new file mode 100644 index 00000000..3297019b --- /dev/null +++ b/TestData/t35_tool/meta_003.bin @@ -0,0 +1 @@ + !"#$%&'()*+,-./ \ No newline at end of file diff --git a/TestData/t35_tool/test_all_modes.sh b/TestData/t35_tool/test_all_modes.sh new file mode 100755 index 00000000..a7ef7141 --- /dev/null +++ b/TestData/t35_tool/test_all_modes.sh @@ -0,0 +1,422 @@ +#\!/usr/bin/env bash +# +# T.35 Tool - Comprehensive Testing Script +# Tests injection modes with matching extraction + auto extraction +# Performs round-trip verification for valid combinations only +# + +set -e # Exit on error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuration +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="${SCRIPT_DIR}/../.." +TEST_DATA_DIR="${PROJECT_ROOT}/TestData/t35_tool" + +# Find t35_tool executable (priority order: bin/, mybuild/, build/, find) +find_tool() { + local tool_path="" + local source="" + + # 1. Check bin/ directory (SET_CUSTOM_OUTPUT_DIRS=ON) + if [ -f "${PROJECT_ROOT}/bin/t35_tool" ]; then + tool_path="${PROJECT_ROOT}/bin/t35_tool" + source="bin/ (custom output dirs)" + # 2. Check mybuild/ directory + elif [ -f "${PROJECT_ROOT}/mybuild/IsoLib/t35_tool/t35_tool" ]; then + tool_path="${PROJECT_ROOT}/mybuild/IsoLib/t35_tool/t35_tool" + source="mybuild/ (build directory)" + # 3. Check build/ directory + elif [ -f "${PROJECT_ROOT}/build/IsoLib/t35_tool/t35_tool" ]; then + tool_path="${PROJECT_ROOT}/build/IsoLib/t35_tool/t35_tool" + source="build/ (build directory)" + # 4. Search for tool + else + tool_path=$(find "${PROJECT_ROOT}" -name "t35_tool" -type f -executable 2>/dev/null | grep -v legacy | head -1) + if [ -n "$tool_path" ]; then + source="found at $(dirname "$tool_path")" + fi + fi + + if [ -z "$tool_path" ] || [ ! -f "$tool_path" ]; then + printf "${RED}[ERROR]${NC} t35_tool not found. Please build the project first.\n" >&2 + printf " Searched locations:\n" >&2 + printf " - ${PROJECT_ROOT}/bin/t35_tool\n" >&2 + printf " - ${PROJECT_ROOT}/mybuild/IsoLib/t35_tool/t35_tool\n" >&2 + printf " - ${PROJECT_ROOT}/build/IsoLib/t35_tool/t35_tool\n" >&2 + return 1 + fi + + printf "${GREEN}[TOOL]${NC} Using t35_tool from: ${BLUE}%s${NC}\n" "$source" >&2 + printf " Path: %s\n\n" "$tool_path" >&2 + + echo "$tool_path" +} + +TOOL=$(find_tool) || exit 1 +INPUT_VIDEO="${PROJECT_ROOT}/TestData/isobmff/01_simple.mp4" +OUTPUT_DIR="${TEST_DATA_DIR}/output_all_modes" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +RESULTS_DIR="${OUTPUT_DIR}/${TIMESTAMP}" + +# Test configuration +T35_PREFIX="B500900001:SMPTE-ST2094-50" +SOURCE_MANIFEST="${TEST_DATA_DIR}/test_manifest.json" + +# Injection modes to test +INJECTION_MODES=( + "mebx-me4c" + "dedicated-it35" + "sample-group" +) + +# Results tracking (simple arrays instead of associative array) +RESULTS_KEYS=() +RESULTS_VALUES=() +TOTAL_TESTS=0 +PASSED_TESTS=0 +FAILED_TESTS=0 + +# Helper function to store result +store_result() { + local key="$1" + local value="$2" + RESULTS_KEYS+=("$key") + RESULTS_VALUES+=("$value") +} + +# Helper function to get result +get_result() { + local key="$1" + local i + local len=${#RESULTS_KEYS[@]} + for ((i=0; i "${log_file}" 2>&1; then + + PASSED_TESTS=$((PASSED_TESTS + 1)) + store_result "inject_${injection_mode}" "PASS" + log_success "Injection successful: ${injection_mode}" + + # Get file size + local size=$(stat -f%z "${output_file}" 2>/dev/null || stat -c%s "${output_file}" 2>/dev/null) + log_info "Output file size: ${size} bytes" + else + FAILED_TESTS=$((FAILED_TESTS + 1)) + store_result "inject_${injection_mode}" "FAIL" + log_error "Injection failed: ${injection_mode}" + log_info "Check log: ${log_file}" + fi +} + +# Test extraction +test_extraction() { + local injection_mode=$1 + local extraction_mode=$2 + local injected_file="${RESULTS_DIR}/injected/${injection_mode}.mp4" + local extract_dir="${RESULTS_DIR}/extracted/${injection_mode}_${extraction_mode}" + local log_file="${RESULTS_DIR}/logs/extract_${injection_mode}_${extraction_mode}.log" + + log_info "Extracting ${injection_mode} with ${extraction_mode}..." + + TOTAL_TESTS=$((TOTAL_TESTS + 1)) + + if [ \! -f "${injected_file}" ]; then + log_warn "Skipping (injected file missing): ${injection_mode} -> ${extraction_mode}" + store_result "extract_${injection_mode}_${extraction_mode}" "SKIP" + return + fi + + if "${TOOL}" extract "${injected_file}" "${extract_dir}" \ + --method "${extraction_mode}" \ + --t35-prefix "${T35_PREFIX}" \ + > "${log_file}" 2>&1; then + + PASSED_TESTS=$((PASSED_TESTS + 1)) + store_result "extract_${injection_mode}_${extraction_mode}" "PASS" + log_success "Extraction successful: ${injection_mode} -> ${extraction_mode}" + + # Count extracted files + local count=$(ls -1 "${extract_dir}"/metadata_*.bin 2>/dev/null | wc -l | tr -d ' ') + log_info "Extracted ${count} metadata files" + else + FAILED_TESTS=$((FAILED_TESTS + 1)) + store_result "extract_${injection_mode}_${extraction_mode}" "FAIL" + log_error "Extraction failed: ${injection_mode} -> ${extraction_mode}" + log_info "Check log: ${log_file}" + fi +} + +# Verify round-trip +verify_roundtrip() { + local injection_mode=$1 + local extraction_mode=$2 + local extract_dir="${RESULTS_DIR}/extracted/${injection_mode}_${extraction_mode}" + local diff_file="${RESULTS_DIR}/diffs/${injection_mode}_${extraction_mode}.diff" + + log_info "Verifying round-trip: ${injection_mode} -> ${extraction_mode}" + + TOTAL_TESTS=$((TOTAL_TESTS + 1)) + + if [ \! -d "${extract_dir}" ]; then + log_warn "Skipping verification (extraction failed)" + store_result "verify_${injection_mode}_${extraction_mode}" "SKIP" + return + fi + + # Compare each extracted file with original + local all_match=true + for i in 1 2 3; do + local original="${TEST_DATA_DIR}/meta_00${i}.bin" + local extracted="${extract_dir}/metadata_${i}.bin" + + if [ \! -f "${extracted}" ]; then + log_error "Missing extracted file: metadata_${i}.bin" + all_match=false + echo "Missing: metadata_${i}.bin" >> "${diff_file}" + continue + fi + + if \! diff -q "${original}" "${extracted}" > /dev/null 2>&1; then + log_error "Mismatch: metadata_${i}.bin" + all_match=false + diff "${original}" "${extracted}" >> "${diff_file}" 2>&1 || true + fi + done + + if [ "$all_match" = true ]; then + PASSED_TESTS=$((PASSED_TESTS + 1)) + store_result "verify_${injection_mode}_${extraction_mode}" "PASS" + log_success "Round-trip verified: ${injection_mode} -> ${extraction_mode}" + else + FAILED_TESTS=$((FAILED_TESTS + 1)) + store_result "verify_${injection_mode}_${extraction_mode}" "FAIL" + log_error "Round-trip verification failed: ${injection_mode} -> ${extraction_mode}" + log_info "Check diff: ${diff_file}" + fi +} + +# Generate summary report +generate_report() { + local report_file="${RESULTS_DIR}/TEST_REPORT.txt" + + log_section "Generating Test Report" + + { + echo "=========================================" + echo "T.35 Tool - Test Report" + echo "=========================================" + echo "Date: $(date)" + echo "Results Directory: ${RESULTS_DIR}" + echo "" + echo "Configuration:" + echo " Tool: ${TOOL}" + echo " Input Video: ${INPUT_VIDEO}" + echo " T.35 Prefix: ${T35_PREFIX}" + echo "" + echo "Test Summary:" + echo " Total Tests: ${TOTAL_TESTS}" + echo " Passed: ${PASSED_TESTS}" + echo " Failed: ${FAILED_TESTS}" + echo "" + echo "=========================================" + echo "Injection Tests" + echo "=========================================" + for mode in "${INJECTION_MODES[@]}"; do + local result=$(get_result "inject_${mode}") + printf " %-20s %s\n" "${mode}:" "${result}" + done + echo "" + echo "=========================================" + echo "Extraction Tests" + echo "=========================================" + for inj_mode in "${INJECTION_MODES[@]}"; do + echo "" + echo " ${inj_mode}:" + # Show matching extraction mode + local result=$(get_result "extract_${inj_mode}_${inj_mode}") + printf " %-20s %s\n" "${inj_mode}:" "${result}" + # Show auto extraction + local result_auto=$(get_result "extract_${inj_mode}_auto") + printf " %-20s %s\n" "auto:" "${result_auto}" + done + echo "" + echo "=========================================" + echo "Round-Trip Verification" + echo "=========================================" + for inj_mode in "${INJECTION_MODES[@]}"; do + echo "" + echo " ${inj_mode}:" + # Show matching extraction mode + local result=$(get_result "verify_${inj_mode}_${inj_mode}") + printf " %-20s %s\n" "${inj_mode}:" "${result}" + # Show auto extraction + local result_auto=$(get_result "verify_${inj_mode}_auto") + printf " %-20s %s\n" "auto:" "${result_auto}" + done + echo "" + echo "=========================================" + echo "Files Generated" + echo "=========================================" + echo " Injected MP4 files: ${RESULTS_DIR}/injected/" + echo " Extracted metadata: ${RESULTS_DIR}/extracted/" + echo " Diff files: ${RESULTS_DIR}/diffs/" + echo " Log files: ${RESULTS_DIR}/logs/" + echo "" + } | tee "${report_file}" + + log_success "Report saved to: ${report_file}" +} + +# Main test execution +main() { + log_section "T.35 Tool - Comprehensive Test Suite" + + setup_directories + check_prerequisites + + # Test all injection modes + log_section "Testing Injection Modes" + for mode in "${INJECTION_MODES[@]}"; do + test_injection "${mode}" + done + printf "\n" + + # Test extraction: each injection mode with matching extractor + auto + log_section "Testing Extraction Modes" + for inj_mode in "${INJECTION_MODES[@]}"; do + log_info "Testing extractions from: ${inj_mode}" + # Test with matching extraction mode + test_extraction "${inj_mode}" "${inj_mode}" + # Test with auto extraction + test_extraction "${inj_mode}" "auto" + printf "\n" + done + + # Verify round-trips: each injection mode with matching extractor + auto + log_section "Verifying Round-Trip Integrity" + for inj_mode in "${INJECTION_MODES[@]}"; do + log_info "Verifying round-trips for: ${inj_mode}" + # Verify with matching extraction mode + verify_roundtrip "${inj_mode}" "${inj_mode}" + # Verify with auto extraction + verify_roundtrip "${inj_mode}" "auto" + printf "\n" + done + + # Generate report + generate_report + + # Final summary + log_section "Test Execution Complete" + if [ ${FAILED_TESTS} -eq 0 ]; then + log_success "All tests passed\! (${PASSED_TESTS}/${TOTAL_TESTS})" + exit 0 + else + log_error "Some tests failed: ${FAILED_TESTS}/${TOTAL_TESTS}" + log_info "Check results in: ${RESULTS_DIR}" + exit 1 + fi +} + +# Run main +main diff --git a/TestData/t35_tool/test_manifest.json b/TestData/t35_tool/test_manifest.json new file mode 100644 index 00000000..0b94ccbb --- /dev/null +++ b/TestData/t35_tool/test_manifest.json @@ -0,0 +1,20 @@ +{ + "t35_prefix": "B500900001", + "items": [ + { + "frame_start": 0, + "frame_duration": 24, + "binary_file": "meta_001.bin" + }, + { + "frame_start": 24, + "frame_duration": 24, + "binary_file": "meta_002.bin" + }, + { + "frame_start": 48, + "frame_duration": 24, + "binary_file": "meta_003.bin" + } + ] +} diff --git a/TestData/t35_tool/test_smpte2094-50.sh b/TestData/t35_tool/test_smpte2094-50.sh new file mode 100755 index 00000000..fda55845 --- /dev/null +++ b/TestData/t35_tool/test_smpte2094-50.sh @@ -0,0 +1,211 @@ +#!/usr/bin/env bash +# +# T.35 Tool - SMPTE ST 2094-50 Testing Script +# Tests injection and extraction with real SMPTE ST 2094-50 metadata +# + +set -e # Exit on error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuration +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="${SCRIPT_DIR}/../.." +TEST_DATA_DIR="${SCRIPT_DIR}" + +# Find t35_tool executable (priority order: bin/, mybuild/, build/, find) +find_tool() { + local tool_path="" + local source="" + + # 1. Check bin/ directory (SET_CUSTOM_OUTPUT_DIRS=ON) + if [ -f "${PROJECT_ROOT}/bin/t35_tool" ]; then + tool_path="${PROJECT_ROOT}/bin/t35_tool" + source="bin/ (custom output dirs)" + # 2. Check mybuild/ directory + elif [ -f "${PROJECT_ROOT}/mybuild/IsoLib/t35_tool/t35_tool" ]; then + tool_path="${PROJECT_ROOT}/mybuild/IsoLib/t35_tool/t35_tool" + source="mybuild/ (build directory)" + # 3. Check build/ directory + elif [ -f "${PROJECT_ROOT}/build/IsoLib/t35_tool/t35_tool" ]; then + tool_path="${PROJECT_ROOT}/build/IsoLib/t35_tool/t35_tool" + source="build/ (build directory)" + # 4. Search for tool + else + tool_path=$(find "${PROJECT_ROOT}" -name "t35_tool" -type f -executable 2>/dev/null | grep -v legacy | head -1) + if [ -n "$tool_path" ]; then + source="found at $(dirname "$tool_path")" + fi + fi + + if [ -z "$tool_path" ] || [ ! -f "$tool_path" ]; then + printf "${RED}[ERROR]${NC} t35_tool not found. Please build the project first.\n" >&2 + printf " Searched locations:\n" >&2 + printf " - ${PROJECT_ROOT}/bin/t35_tool\n" >&2 + printf " - ${PROJECT_ROOT}/mybuild/IsoLib/t35_tool/t35_tool\n" >&2 + printf " - ${PROJECT_ROOT}/build/IsoLib/t35_tool/t35_tool\n" >&2 + return 1 + fi + + printf "${GREEN}[TOOL]${NC} Using t35_tool from: ${BLUE}%s${NC}\n" "$source" >&2 + printf " Path: %s\n\n" "$tool_path" >&2 + + echo "$tool_path" +} + +TOOL=$(find_tool) || exit 1 + +# Test configuration +INPUT_VIDEO="${TEST_DATA_DIR}/ST2094-50_LightDetector.mov" +OUTPUT_DIR="${TEST_DATA_DIR}/output_smpte" +T35_PREFIX="B500900001:SMPTE-ST2094-50" + +# Injection methods to test +METHODS=( + "mebx-me4c" + "dedicated-it35" +) + +# SMPTE test folders +SMPTE_FOLDERS=( + "NoAdaptiveToneMap" + "DefaultToneMapRWTMO" + "CustomTMO" +) + +# Helper functions +log_info() { + printf "${BLUE}[INFO]${NC} %s\n" "$1" +} + +log_success() { + printf "${GREEN}[PASS]${NC} %s\n" "$1" +} + +log_error() { + printf "${RED}[FAIL]${NC} %s\n" "$1" +} + +log_section() { + printf "\n" + printf "${BLUE}========================================${NC}\n" + printf "${BLUE}%s${NC}\n" "$1" + printf "${BLUE}========================================${NC}\n" +} + +# Check prerequisites +check_prerequisites() { + log_section "Checking Prerequisites" + + if [ ! -f "${INPUT_VIDEO}" ]; then + log_error "Input video not found: ${INPUT_VIDEO}" + exit 1 + fi + log_success "Input video found: ${INPUT_VIDEO}" + + for folder in "${SMPTE_FOLDERS[@]}"; do + if [ ! -d "${TEST_DATA_DIR}/${folder}" ]; then + log_error "SMPTE folder not found: ${TEST_DATA_DIR}/${folder}" + exit 1 + fi + done + log_success "All SMPTE folders present" + + printf "\n" +} + +# Test a single SMPTE folder with a specific method +test_smpte_folder() { + local folder=$1 + local method=$2 + local injected_file="${OUTPUT_DIR}/${folder}_${method}.mov" + local extract_dir="${OUTPUT_DIR}/${folder}_${method}_extracted" + local inject_log="${OUTPUT_DIR}/${folder}_${method}_inject.log" + local extract_log="${OUTPUT_DIR}/${folder}_${method}_extract.log" + + log_section "Testing: ${folder} with ${method}" + + # Inject + log_info "Injecting SMPTE ST 2094-50 metadata from ${folder} using ${method}..." + if "${TOOL}" inject "${INPUT_VIDEO}" "${injected_file}" \ + --source "smpte-folder:${TEST_DATA_DIR}/${folder}" \ + --method "${method}" \ + --t35-prefix "${T35_PREFIX}" \ + > "${inject_log}" 2>&1; then + log_success "Injection successful" + else + log_error "Injection failed - check ${inject_log}" + return 1 + fi + + # Extract + log_info "Extracting metadata with auto-detection..." + if "${TOOL}" extract "${injected_file}" "${extract_dir}" \ + --method "auto" \ + --t35-prefix "${T35_PREFIX}" \ + > "${extract_log}" 2>&1; then + log_success "Extraction successful" + + # Count extracted files + local count=$(ls -1 "${extract_dir}"/metadata_*.bin 2>/dev/null | wc -l | tr -d ' ') + log_info "Extracted ${count} metadata files" + else + log_error "Extraction failed - check ${extract_log}" + return 1 + fi + + printf "\n" +} + +# Main execution +main() { + log_section "T.35 Tool - SMPTE ST 2094-50 Tests" + + log_info "Test configuration:" + log_info " Input video: ${INPUT_VIDEO}" + log_info " Output directory: ${OUTPUT_DIR}" + log_info " Injection methods: ${METHODS[*]}" + log_info " T.35 prefix: ${T35_PREFIX}" + printf "\n" + + # Setup + mkdir -p "${OUTPUT_DIR}" + check_prerequisites + + # Test all SMPTE folders with all methods + local failed=0 + local total=0 + for method in "${METHODS[@]}"; do + log_section "Testing with method: ${method}" + for folder in "${SMPTE_FOLDERS[@]}"; do + total=$((total + 1)) + if ! test_smpte_folder "${folder}" "${method}"; then + failed=$((failed + 1)) + fi + done + done + + # Summary + log_section "Test Summary" + log_info "Total tests: ${total} (${#METHODS[@]} methods × ${#SMPTE_FOLDERS[@]} folders)" + log_info "Passed: $((total - failed))" + log_info "Failed: ${failed}" + printf "\n" + + if [ ${failed} -eq 0 ]; then + log_success "All SMPTE tests passed!" + exit 0 + else + log_error "${failed} test(s) failed" + log_info "Check logs in: ${OUTPUT_DIR}" + exit 1 + fi +} + +# Run main +main diff --git a/test/test_01_simple.cpp b/test/test_01_simple.cpp index 9fe09686..a85d2bc6 100644 --- a/test/test_01_simple.cpp +++ b/test/test_01_simple.cpp @@ -28,7 +28,7 @@ #include const std::string strDataPath = TESTDATA_PATH; -const std::string strTestFile = strDataPath + +"/isobmff/01_simple.mp4"; +const std::string strTestFile = strDataPath + "/isobmff/01_simple.mp4"; // isobmff stuff ISOMovie cMovieBox; diff --git a/test/test_data.h b/test/test_data.h index 8e6d9338..402ab52e 100644 --- a/test/test_data.h +++ b/test/test_data.h @@ -97,6 +97,13 @@ const u8 auFR[] = {0x28, 0x01, 0xAF, 0x78, 0xEB, 0x27, 0x7F, 0xFD, 0xCE, 0x7C, 0 0x10, 0xE3, 0x10, 0x50, 0xC4, 0x13, 0x88, 0x05, 0x8B, 0x60, 0xFB, 0x13, 0x89, 0x7C, 0x54, 0x50, 0x71, 0xBA, 0xE5, 0x24, 0x98}; +const u8 SEI_HDR[] = {0x4E, 0x01, 0x04, 0x40, 0xB5, 0x00, 0x3C, 0x00, 0x01, 0x04, 0x01, 0x40, + 0x00, 0x0F, 0xA3, 0x0D, 0x41, 0x86, 0xA0, 0xC3, 0x50, 0x49, 0xA8, 0xA4, + 0x08, 0x00, 0x00, 0x2E, 0x1A, 0x80, 0x50, 0x00, 0x34, 0xC8, 0x00, 0x01, + 0x90, 0x74, 0x76, 0x5A, 0x2D, 0x42, 0xD2, 0x41, 0xDE, 0xFA, 0x57, 0x47, + 0x1A, 0x84, 0x80, 0x00, 0x40, 0x1C, 0x0F, 0xA5, 0xFA, 0x60, 0x9E, 0xD9, + 0x0A, 0x85, 0xB1, 0xB1, 0x9D, 0xDF, 0xBF, 0x00, 0x80}; + } // namespace HEVC /// One file-level meta with 'test' handler and 2 EntityToGroups: diff --git a/test/test_helpers.h b/test/test_helpers.h index 9a269b6b..94b62941 100644 --- a/test/test_helpers.h +++ b/test/test_helpers.h @@ -29,6 +29,8 @@ #include #include "test_data.h" +#include + inline int parseVVCNal(FILE *input, u8 **data, int *data_len) { size_t startPos; @@ -185,95 +187,67 @@ inline std::vector getMetaSample(u32 x, u32 y, u32 w, u32 h) * @param repeatPattern number of times to repeat the pattern. No samples are added if this is 0 * @param sampleEntryH sample entry handle (for the first call) * @param lengthSize the length in bytes of the NALUnitLength field in an HEVC video sample + * @param add_sei if set adds an SEI message in front of each frame * @return MP4Err error code */ inline MP4Err addHEVCSamples(MP4Media media, std::string strPattern, u32 repeatPattern = 1, - MP4Handle sampleEntryH = 0, u32 lengthSize = 1) + MP4Handle sampleEntryH = 0, u32 lengthSize = 1, bool add_sei = false) { MP4Err err; - u32 sampleCount = 0; MP4Handle sampleDataH, durationsH, sizesH; - err = MP4NewHandle(sizeof(u32), &durationsH); - CHECK(err == MP4NoErr); + err = MP4NewHandle(sizeof(u32), &durationsH); *((u32 *)*durationsH) = TIMESCALE / FPS; std::vector bufferData; std::vector bufferSizes; - for(std::string::const_iterator it = strPattern.cbegin(); it != strPattern.cend(); ++it) + + // a lambda function to add NAL units + auto addNALUnit = [&](const u8* data, u32 size) { - switch(*it) + u32 totalSize = 0; + if(add_sei) { - case 'r': - appendDataWithLengthField(bufferData, lengthSize, HEVC::auRed, sizeof(HEVC::auRed)); - bufferSizes.push_back(sizeof(HEVC::auRed) + lengthSize); - break; - case 'b': - appendDataWithLengthField(bufferData, lengthSize, HEVC::auBlue, sizeof(HEVC::auBlue)); - bufferSizes.push_back(sizeof(HEVC::auBlue) + lengthSize); - break; - case 'g': - appendDataWithLengthField(bufferData, lengthSize, HEVC::auGreen, sizeof(HEVC::auGreen)); - bufferSizes.push_back(sizeof(HEVC::auGreen) + lengthSize); - break; - case 'y': - appendDataWithLengthField(bufferData, lengthSize, HEVC::auYellow, sizeof(HEVC::auYellow)); - bufferSizes.push_back(sizeof(HEVC::auYellow) + lengthSize); - break; - case 'w': - appendDataWithLengthField(bufferData, lengthSize, HEVC::auWhite, sizeof(HEVC::auWhite)); - bufferSizes.push_back(sizeof(HEVC::auWhite) + lengthSize); - break; - case 'k': - appendDataWithLengthField(bufferData, lengthSize, HEVC::auBlack, sizeof(HEVC::auBlack)); - bufferSizes.push_back(sizeof(HEVC::auBlack) + lengthSize); - break; - case 'R': - appendDataWithLengthField(bufferData, lengthSize, HEVC::auRU, sizeof(HEVC::auRU)); - bufferSizes.push_back(sizeof(HEVC::auRU) + lengthSize); - break; - case 'U': - appendDataWithLengthField(bufferData, lengthSize, HEVC::auUA, sizeof(HEVC::auUA)); - bufferSizes.push_back(sizeof(HEVC::auUA) + lengthSize); - break; - case 'D': - appendDataWithLengthField(bufferData, lengthSize, HEVC::auDE, sizeof(HEVC::auDE)); - bufferSizes.push_back(sizeof(HEVC::auDE) + lengthSize); - break; - case 'F': - appendDataWithLengthField(bufferData, lengthSize, HEVC::auFR, sizeof(HEVC::auFR)); - bufferSizes.push_back(sizeof(HEVC::auFR) + lengthSize); - break; - case 'N': - appendDataWithLengthField(bufferData, lengthSize, HEVC::auNL, sizeof(HEVC::auNL)); - bufferSizes.push_back(sizeof(HEVC::auNL) + lengthSize); - break; - case 'I': - appendDataWithLengthField(bufferData, lengthSize, HEVC::auID, sizeof(HEVC::auID)); - bufferSizes.push_back(sizeof(HEVC::auID) + lengthSize); - break; - default: - break; + appendDataWithLengthField(bufferData, lengthSize, HEVC::SEI_HDR, sizeof(HEVC::SEI_HDR)); + totalSize += sizeof(HEVC::SEI_HDR) + lengthSize; + } + appendDataWithLengthField(bufferData, lengthSize, data, size); + totalSize += size + lengthSize; + bufferSizes.push_back(totalSize); + }; + + for (char c : strPattern) + { + switch(c) { + case 'r': addNALUnit(HEVC::auRed, sizeof(HEVC::auRed)); break; + case 'b': addNALUnit(HEVC::auBlue, sizeof(HEVC::auBlue)); break; + case 'g': addNALUnit(HEVC::auGreen, sizeof(HEVC::auGreen)); break; + case 'y': addNALUnit(HEVC::auYellow, sizeof(HEVC::auYellow)); break; + case 'w': addNALUnit(HEVC::auWhite, sizeof(HEVC::auWhite)); break; + case 'k': addNALUnit(HEVC::auBlack, sizeof(HEVC::auBlack)); break; + case 'R': addNALUnit(HEVC::auRU, sizeof(HEVC::auRU)); break; + case 'U': addNALUnit(HEVC::auUA, sizeof(HEVC::auUA)); break; + case 'D': addNALUnit(HEVC::auDE, sizeof(HEVC::auDE)); break; + case 'F': addNALUnit(HEVC::auFR, sizeof(HEVC::auFR)); break; + case 'N': addNALUnit(HEVC::auNL, sizeof(HEVC::auNL)); break; + case 'I': addNALUnit(HEVC::auID, sizeof(HEVC::auID)); break; + default: break; } } // repeat pattern std::vector bufferDataPattern = bufferData; std::vector bufferSizesPattern = bufferSizes; - std::string fullPattern = strPattern; for(u32 n = 1; n < repeatPattern; ++n) { bufferData.insert(bufferData.end(), bufferDataPattern.begin(), bufferDataPattern.end()); bufferSizes.insert(bufferSizes.end(), bufferSizesPattern.begin(), bufferSizesPattern.end()); - fullPattern += strPattern; } // create handles and copy data err = MP4NewHandle(bufferData.size() * sizeof(u8), &sampleDataH); - CHECK(err == MP4NoErr); std::memcpy((*sampleDataH), bufferData.data(), bufferData.size() * sizeof(u8)); err = MP4NewHandle(sizeof(u32) * bufferSizes.size(), &sizesH); - CHECK(err == MP4NoErr); for(u32 n = 0; n < bufferSizes.size(); n++) { ((u32 *)*sizesH)[n] = bufferSizes[n]; @@ -283,12 +257,9 @@ inline MP4Err addHEVCSamples(MP4Media media, std::string strPattern, u32 repeatP durationsH, sizesH, sampleEntryH, 0, 0); CHECK(err == MP4NoErr); - err = MP4DisposeHandle(sampleDataH); - CHECK(err == MP4NoErr); - err = MP4DisposeHandle(durationsH); - CHECK(err == MP4NoErr); - err = MP4DisposeHandle(sizesH); - CHECK(err == MP4NoErr); + MP4DisposeHandle(sampleDataH); + MP4DisposeHandle(durationsH); + MP4DisposeHandle(sizesH); return err; } @@ -430,11 +401,9 @@ inline MP4Err addMebxSamples(MP4Media media, std::string strPattern, u32 repeatP u32 lk_w = 0, u32 lk_k = 0, u32 lk_g = 0) { MP4Err err; - u32 sampleCount = 0; MP4Handle sampleDataH, durationsH, sizesH; - err = MP4NewHandle(sizeof(u32), &durationsH); - CHECK(err == MP4NoErr); + err = MP4NewHandle(sizeof(u32), &durationsH); *((u32 *)*durationsH) = TIMESCALE / FPS; std::vector bufferData; @@ -557,6 +526,13 @@ inline MP4Err addMebxSamples(MP4Media media, std::string strPattern, u32 repeatP bufferSizes.push_back(sampleSize); break; } + case 'T': + { + std::vector metaSample = {0xDE, 0xAD, 0xBE, 0xEF}; + appendDataWithBoxField(bufferData, lk_r, metaSample); + bufferSizes.push_back(metaSample.size() + 8); + break; + } default: break; } @@ -575,10 +551,8 @@ inline MP4Err addMebxSamples(MP4Media media, std::string strPattern, u32 repeatP // create handles and copy data err = MP4NewHandle(bufferData.size() * sizeof(u8), &sampleDataH); - CHECK(err == MP4NoErr); std::memcpy((*sampleDataH), bufferData.data(), bufferData.size() * sizeof(u8)); err = MP4NewHandle(sizeof(u32) * bufferSizes.size(), &sizesH); - CHECK(err == MP4NoErr); for(u32 n = 0; n < bufferSizes.size(); n++) { ((u32 *)*sizesH)[n] = bufferSizes[n]; @@ -588,12 +562,9 @@ inline MP4Err addMebxSamples(MP4Media media, std::string strPattern, u32 repeatP durationsH, sizesH, sampleEntryH, 0, 0); CHECK(err == MP4NoErr); - err = MP4DisposeHandle(sampleDataH); - CHECK(err == MP4NoErr); - err = MP4DisposeHandle(durationsH); - CHECK(err == MP4NoErr); - err = MP4DisposeHandle(sizesH); - CHECK(err == MP4NoErr); + MP4DisposeHandle(sampleDataH); + MP4DisposeHandle(durationsH); + MP4DisposeHandle(sizesH); return err; } @@ -626,7 +597,6 @@ inline MP4Err checkRedMebxSamples(std::string strPattern, u32 repeatPattern, MP4 { u32 redSize = 0; err = MP4GetHandleSize(auData, &redSize); - CHECK(err == MP4NoErr); CHECK(0 == redSize); break; } diff --git a/test/test_mebx.cpp b/test/test_mebx.cpp index cf8fe430..e67f6296 100644 --- a/test/test_mebx.cpp +++ b/test/test_mebx.cpp @@ -32,6 +32,7 @@ TEST_CASE("mebx") std::string strPattern = "NIbFD"; u32 repeatPattern = 6; std::string strMebxMe4cFile = "test_mebx_me4c.mp4"; + std::string strMebxT35File = "test_mebx_t35.mp4"; std::string strUnMebxFile = "test_unmebx.mp4"; std::string strReMebxFile = "test_remebx.mp4"; @@ -241,7 +242,7 @@ TEST_CASE("mebx") } - err = MP4SetMebxTrackReader(reader, MP4_FOUR_CHAR_CODE('r', 'e', 'd', 'd')); + err = MP4SetMebxTrackReaderLocalKeyId(reader, MP4_FOUR_CHAR_CODE('r', 'e', 'd', 'd')); CHECK(err == MP4NoErr); u32 n = 0; @@ -263,4 +264,76 @@ TEST_CASE("mebx") } + + SECTION("T.35") + { + MP4Movie moov; + MP4Track trakV; + MP4Track trakM; + MP4Media mediaV; + MP4Media mediaM; + + MP4Handle spsHandle, ppsHandle, vpsHandle, sampleEntryVH, sampleEntryMH; + err = MP4NewHandle(sizeof(HEVC::SPS), &spsHandle); + std::memcpy((*spsHandle), HEVC::SPS, sizeof(HEVC::SPS)); + err = MP4NewHandle(sizeof(HEVC::PPS), &ppsHandle); + std::memcpy((*ppsHandle), HEVC::PPS, sizeof(HEVC::PPS)); + err = MP4NewHandle(sizeof(HEVC::VPS), &vpsHandle); + std::memcpy((*vpsHandle), HEVC::VPS, sizeof(HEVC::VPS)); + err = MP4NewHandle(0, &sampleEntryVH); + err = MP4NewHandle(0, &sampleEntryMH); + + err = MP4NewMovie(&moov, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff); + + // create video track + err = MP4NewMovieTrack(moov, MP4NewTrackIsVisual, &trakV); + err = MP4NewTrackMedia(trakV, &mediaV, MP4VisualHandlerType, TIMESCALE, NULL); + err = ISONewHEVCSampleDescription(trakV, sampleEntryVH, 1, 1, spsHandle, ppsHandle, vpsHandle); + err = addHEVCSamples(mediaV, "", 0, sampleEntryVH); + err = addHEVCSamples(mediaV, strPattern, repeatPattern); + + // create mebx track + err = MP4NewMovieTrack(moov, MP4NewTrackIsMebx, &trakM); + err = MP4NewTrackMedia(trakM, &mediaM, MP4MetaHandlerType, TIMESCALE, NULL); + err = MP4AddTrackReference(trakM, trakV, MP4DescTrackReferenceType, 0); + + // create mebx sample entry + MP4BoxedMetadataSampleEntryPtr mebx; + err = ISONewMebxSampleDescription(&mebx, 1); + + MP4Handle dmcvtH; + MP4NewHandle(30, &dmcvtH); + + // first 5 bytes: binary prefix according to generic definition + (*dmcvtH)[0] = 0xB5; + (*dmcvtH)[1] = 0x00; + (*dmcvtH)[2] = 0x90; + (*dmcvtH)[3] = 0x00; + (*dmcvtH)[4] = 0x01; + + // rest: ASCII string "smpte_st_2094_50_dmcvt_v1" + const char dmcvStr[] = "smpte_st_2094_50_dmcvt_v1"; + std::memcpy(&(*dmcvtH)[5], dmcvStr, sizeof(dmcvStr) - 1); + + u32 local_key_id; + err = ISOAddMebxMetadataToSampleEntry(mebx, 1, &local_key_id, MP4_FOUR_CHAR_CODE('i', 't', '3', '5'), dmcvtH, 0, 0); + CHECK(err == MP4NoErr); + + // add mebx sample entry to track's media + err = ISOGetMebxHandle(mebx, sampleEntryMH); + CHECK(err == MP4NoErr); + err = addMebxSamples(mediaM, "", 0, sampleEntryMH); + CHECK(err == MP4NoErr); + + // add samples using local_key_id (hijack red id and use all Ts in pattern) + strPattern.assign(strPattern.size(), 'T'); + err = addMebxSamples(mediaM, strPattern, repeatPattern, 0, local_key_id); + CHECK(err == MP4NoErr); + + // write file + err = MP4EndMediaEdits(mediaV); + err = MP4EndMediaEdits(mediaM); + err = MP4WriteMovieToFile(moov, strMebxT35File.c_str()); + CHECK(err == MP4NoErr); + } } diff --git a/test/test_sample_groups.cpp b/test/test_sample_groups.cpp index 7334e8e2..9f3d66d8 100644 --- a/test/test_sample_groups.cpp +++ b/test/test_sample_groups.cpp @@ -315,8 +315,9 @@ TEST_CASE("sample_groups") u32 temp = 0; err = addGroupDescription(media, FOURCC_COLOR, "Red frames", temp); - // this must fail because "Red frames" payload is already added with the same type - CHECK(err != ISONoErr); + // this must succeed - find-or-add behavior reuses existing entry with same type and payload + CHECK(err == ISONoErr); + CHECK(temp == groupIdRed); // should return the existing group ID // just add sample entry, call addHEVCSamples with sample count = 0 err = addHEVCSamples(media, "r", 0, sampleEntryH); @@ -342,8 +343,10 @@ TEST_CASE("sample_groups") // (but it shall not be in defragmented file) err = addGroupDescription(media, FOURCC_TEST, "Test", temp); CHECK(err == ISONoErr); - err = addGroupDescription(media, FOURCC_TEST, "Test", temp); - CHECK(err != ISONoErr); // this must fail because same type and payload already added + u32 temp2 = 0; + err = addGroupDescription(media, FOURCC_TEST, "Test", temp2); + CHECK(err == ISONoErr); // this must succeed - find-or-add behavior reuses existing entry + CHECK(temp2 == temp); // should return the same group ID err = mapSamplesToGroups(media, "rb", groupIdRed, groupIdBlue, groupIdGreen, groupIdYellow, 3); CHECK(err == ISONoErr); diff --git a/test/test_t35.cpp b/test/test_t35.cpp new file mode 100644 index 00000000..613bbf6d --- /dev/null +++ b/test/test_t35.cpp @@ -0,0 +1,189 @@ +/** + * @file test_t35.cpp + * @author Dimitri Podborski + * @brief Perform checks on T.35 + * @version 0.1 + * @date 2025-04-18 + * + * @copyright This software module was originally developed by Apple Computer, Inc. in the course of + * development of MPEG-4. This software module is an implementation of a part of one or more MPEG-4 + * tools as specified by MPEG-4. ISO/IEC gives users of MPEG-4 free license to this software module + * or modifications thereof for use in hardware or software products claiming conformance to MPEG-4. + * Those intending to use this software module in hardware or software products are advised that its + * use may infringe existing patents. The original developer of this software module and his/her + * company, the subsequent editors and their companies, and ISO/IEC have no liability for use of + * this software module or modifications thereof in an implementation. Copyright is not released for + * non MPEG-4 conforming products. Apple Computer, Inc. retains full right to use the code for its + * own purpose, assign or donate the code to a third party and to inhibit third parties from using + * the code for non MPEG-4 conforming products. This copyright notice must be included in all copies + * or derivative works. Copyright (c) 1999. + * + */ + +// fix +// mdat size correction +// 5796 - 85 = 5711 (0x164F) + +// first sample size correction +// 1839 - 85 = 1754 +// 0x07 0x2F -> 0x06 0xDA + +#include +#include "test_helpers.h" +#include "testdataPath.h" +#include + +const std::string strDataPath = TESTDATA_PATH; +const std::string strTestFile = strDataPath + "/isobmff/hvc1_hdr10plus_original.mp4"; + +/** + * @brief Starting point for this testing case + * + */ +TEST_CASE("T35") +{ + std::string strT35Default = "test_samplegroups_t35_defaultHDR10p.mp4"; + + MP4Err err; + + MP4Handle it35_prefix; + MP4NewHandle(5, &it35_prefix); + (*it35_prefix)[0] = 0xB5; + (*it35_prefix)[1] = 0x00; + (*it35_prefix)[2] = 0x3C; + (*it35_prefix)[3] = 0x00; + (*it35_prefix)[4] = 0x01; + + // TODO: implement a command line tool that will take an HEVC mp4 with T.35 SEIs in samples + // then it will parse all these T.35, and try to move them into sample groups to save space. + // SECTION("TBD") + // { + // MP4Err err; + // MP4Movie moov; + // MP4Track trak; + // MP4Media media; + + // err = MP4OpenMovieFile(&moov, strTestFile.c_str(), MP4OpenMovieDebug); + // err = MP4GetMovieIndTrack(moov, 1, &trak); + // err = MP4GetTrackMedia(trak, &media); + + // u32 codecType = 0; + // u32 nalUnitLength = 0; + // err = MP4GetMovieIndTrackSampleEntryType(moov, 1, &codecType); + // err = MP4GetMovieIndTrackNALUnitLength(moov, 1, &nalUnitLength); + // REQUIRE(codecType == ISOHEVCSampleEntryAtomType); + // CHECK(nalUnitLength == 4); + + // u32 sampleCnt = 0; + // err = MP4GetMediaSampleCount(media, &sampleCnt); + // CHECK(30 == sampleCnt); + + // // TBD iterate through samples and look through T.35 SEI NAL Units + // // we need to build up a table that will contain + // // T.35 data and the number of sample the same data was detected in. For example data_blob1 in sample 1,2,5. data_blob2 in samples 3,4,5,6 etc. + + // } + + SECTION("Check creation of it35 default sample group using ISOAddT35GroupDescription") + { + MP4Movie moov; + MP4Media media; + MP4Track trak; + + u32 lengthSize = 4; + u32 temp = 0; + + MP4Handle spsHandle, ppsHandle, vpsHandle, sampleEntryH; + err = MP4NewHandle(sizeof(HEVC::SPS), &spsHandle); + std::memcpy((*spsHandle), HEVC::SPS, sizeof(HEVC::SPS)); + err = MP4NewHandle(sizeof(HEVC::PPS), &ppsHandle); + std::memcpy((*ppsHandle), HEVC::PPS, sizeof(HEVC::PPS)); + err = MP4NewHandle(sizeof(HEVC::VPS), &vpsHandle); + std::memcpy((*vpsHandle), HEVC::VPS, sizeof(HEVC::VPS)); + err = MP4NewHandle(0, &sampleEntryH); + + err = MP4NewMovie(&moov, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff); + REQUIRE(err == MP4NoErr); + + err = MP4NewMovieTrack(moov, MP4NewTrackIsVisual, &trak); + REQUIRE(err == MP4NoErr); + err = MP4AddTrackToMovieIOD(trak); + CHECK(err == MP4NoErr); + err = MP4NewTrackMedia(trak, &media, MP4VisualHandlerType, TIMESCALE, NULL); + REQUIRE(err == MP4NoErr); + + err = MP4BeginMediaEdits(media); + err = ISONewHEVCSampleDescription(trak, sampleEntryH, 1, lengthSize, spsHandle, ppsHandle, vpsHandle); + REQUIRE(err == MP4NoErr); + + // check getter + err = ISOGetGroupDescriptionEntryCount(media, MP4T35SampleGroupEntry, &temp); + CHECK(err == MP4NotFoundErr); + CHECK(temp == 0); + + // just add sample entry, call addHEVCSamples with sample count = 0 + err = addHEVCSamples(media, "", 0, sampleEntryH, lengthSize, true); + CHECK(err == MP4NoErr); + err = MP4EndMediaEdits(media); + CHECK(err == MP4NoErr); + // add samples + err = addHEVCSamples(media, "rb", 3, nullptr, lengthSize, true); + CHECK(err == MP4NoErr); + + // Add T.35 sample group description header. Default sample group (all samples have this header) + err = ISOSetSamplestoGroupType(media, SAMPLE_GROUP_NORMAL); + CHECK(err == MP4NoErr); + err = ISOAddT35GroupDescription(media, it35_prefix, 0, &temp); + CHECK(err == MP4NoErr); + err = ISOGetGroupDescriptionEntryCount(media, MP4T35SampleGroupEntry, &temp); + CHECK(temp == 1); + + err = MP4WriteMovieToFile(moov, strT35Default.c_str()); + CHECK(err == MP4NoErr); + } + + // TODO: Implement default_group_description_index support in getSampleGroupSampleNumbers + // before re-enabling this test. Currently, ISOGetSampleGroupSampleNumbers only returns + // samples with explicit sample-to-group mappings (sbgp atom), but does not handle + // default group assignments via default_group_description_index. + // See SampleTableAtom.c:742 for the incomplete implementation. + /* + SECTION("Check default it35 sample group") + { + MP4Movie moov; + MP4Track trak; + MP4Media media; + + u32 it35_sg_cnt = 0; + u32 *sample_numbers; + u32 sample_cnt = 0; + + err = MP4OpenMovieFile(&moov, strT35Default.c_str(), MP4OpenMovieDebug); + err = MP4GetMovieIndTrack(moov, 1, &trak); + err = MP4GetTrackMedia(trak, &media); + + err = ISOGetGroupDescriptionEntryCount(media, MP4T35SampleGroupEntry, &it35_sg_cnt); + CHECK(err == MP4NoErr); + CHECK(1 == it35_sg_cnt); + + MP4Handle entryH; + u32 size = 0; + MP4NewHandle(0, &entryH); + err = ISOGetGroupDescription(media, MP4T35SampleGroupEntry, 1, entryH); + CHECK(err == MP4NoErr); + MP4GetHandleSize(entryH, &size); + CHECK(6 == size); + + err = ISOGetSampleGroupSampleNumbers(media, MP4T35SampleGroupEntry, 1, &sample_numbers, &sample_cnt); + CHECK(err == MP4NoErr); + + u32 check_sample_cnt = 0; + MP4GetMediaSampleCount(media, &check_sample_cnt); + CHECK(check_sample_cnt > 0); + CHECK(check_sample_cnt == sample_cnt); + + } + */ + + MP4DisposeHandle(it35_prefix); +}