diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml index 8ba6dac8..590035a7 100644 --- a/.github/workflows/clang-format.yml +++ b/.github/workflows/clang-format.yml @@ -6,9 +6,16 @@ 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 + run: | + find IsoLib/libisomediafile \( -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/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..e7fcbec2 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,76 @@ 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); + /************************************************************************************************* * VVC Sample descriptions ************************************************************************************************/ diff --git a/IsoLib/libisomediafile/src/ISOSampleDescriptions.c b/IsoLib/libisomediafile/src/ISOSampleDescriptions.c index 85489a9f..fc4f5c08 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,201 @@ 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; +} 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..417e131f --- /dev/null +++ b/IsoLib/libisomediafile/src/T35MetadataSampleEntry.c @@ -0,0 +1,202 @@ +/* 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; + u32 descLen; + u32 i; + + if(self == NULL) BAILWITHERROR(MP4BadParamErr) + err = self->super->createFromInputStream(s, proto, (char *)inputStream); + if(err) goto bail; + + GETBYTES(6, reserved); + GET16(dataReferenceIndex); + + /* Calculate remaining bytes to read */ + bytesToRead = (s64)(self->size - self->bytesRead); + if(bytesToRead < 0) BAILWITHERROR(MP4BadDataErr); + + if(bytesToRead > 0) + { + /* Read description (null-terminated UTF-8 string) */ + /* First, scan for null terminator to find description length */ + descLen = 0; + for(i = 0; i < (u32)bytesToRead; i++) + { + u8 byte; + err = inputStream->read8(inputStream, (u32 *)&byte, NULL); + if(err) goto bail; + descLen++; + self->bytesRead++; + if(byte == 0) break; + } + + /* Allocate and read description - we need to re-read the bytes */ + /* Rewind by backing up bytesRead */ + self->bytesRead -= descLen; + + self->description = (char *)calloc(descLen, 1); + TESTMALLOC(self->description); + err = inputStream->readData(inputStream, descLen, (char *)self->description, NULL); + if(err) goto bail; + self->bytesRead += descLen; + + /* Recalculate bytes remaining */ + bytesToRead = (s64)(self->size - self->bytesRead); + + /* Read t35_identifier (everything remaining) */ + if(bytesToRead > 0) + { + self->t35_identifier_size = (u32)bytesToRead; + self->t35_identifier = (u8 *)calloc(bytesToRead, 1); + TESTMALLOC(self->t35_identifier); + err = inputStream->readData(inputStream, bytesToRead, (char *)self->t35_identifier, NULL); + if(err) goto bail; + self->bytesRead += (u32)bytesToRead; + } + } + + if(self->bytesRead != self->size) BAILWITHERROR(MP4BadDataErr) + +bail: + 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..90a101e0 --- /dev/null +++ b/IsoLib/t35_tool/av1_tool.cpp @@ -0,0 +1,286 @@ +/** + * @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..9e99b37a --- /dev/null +++ b/IsoLib/t35_tool/common/Logger.cpp @@ -0,0 +1,45 @@ +#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..5cb7962d --- /dev/null +++ b/IsoLib/t35_tool/common/MetadataTypes.cpp @@ -0,0 +1,87 @@ +#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..46069728 --- /dev/null +++ b/IsoLib/t35_tool/common/T35Prefix.cpp @@ -0,0 +1,145 @@ +#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..def8eab4 --- /dev/null +++ b/IsoLib/t35_tool/extraction/AutoExtractor.cpp @@ -0,0 +1,53 @@ +#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..cce5f515 --- /dev/null +++ b/IsoLib/t35_tool/extraction/DedicatedIt35Extractor.cpp @@ -0,0 +1,356 @@ +#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 +#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); + + // Get T35MetadataSampleEntry to read description and t35_identifier + MP4T35MetadataSampleEntryPtr it35Entry = (MP4T35MetadataSampleEntryPtr)(*sampleEntryH); + + if (it35Entry && it35Entry->t35_identifier && it35Entry->t35_identifier_size > 0) { + // Convert t35_identifier bytes to hex string + std::string hexStr; + hexStr.reserve(it35Entry->t35_identifier_size * 2); + for (u32 i = 0; i < it35Entry->t35_identifier_size; i++) { + char buf[3]; + snprintf(buf, sizeof(buf), "%02X", it35Entry->t35_identifier[i]); + hexStr += buf; + } + + // Build prefix string: "HEX:Description" + std::string filePrefix = hexStr; + if (it35Entry->description && it35Entry->description[0] != '\0') { + filePrefix += ":"; + filePrefix += it35Entry->description; + LOG_DEBUG("Found T35 identifier: {} with description: '{}'", hexStr, it35Entry->description); + } else { + LOG_DEBUG("Found T35 identifier: {} (no description)", hexStr); + } + + // Parse both prefixes to compare hex part only + T35Prefix requestedPrefix(t35PrefixStr); + T35Prefix filePrefixParsed(filePrefix); + + // Verify hex prefix matches (ignore description) + if (requestedPrefix.hex() != filePrefixParsed.hex()) { + LOG_DEBUG("T35 hex '{}' does not match requested hex '{}'", + filePrefixParsed.hex(), requestedPrefix.hex()); + MP4DisposeHandle(sampleEntryH); + continue; // Try next track + } + LOG_DEBUG("T35 hex matches requested hex"); + + // Warn if descriptions differ (informative only) + 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()); + } + } else { + LOG_WARN("Could not read t35_identifier from IT35 sample entry"); + } + + MP4DisposeHandle(sampleEntryH); + + // 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..49263e4e --- /dev/null +++ b/IsoLib/t35_tool/extraction/ExtractionStrategy.cpp @@ -0,0 +1,30 @@ +#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..c6b776a7 --- /dev/null +++ b/IsoLib/t35_tool/extraction/MebxMe4cExtractor.cpp @@ -0,0 +1,501 @@ +#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..e895c088 --- /dev/null +++ b/IsoLib/t35_tool/extraction/SampleGroupExtractor.cpp @@ -0,0 +1,323 @@ +#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..e61bb2e8 --- /dev/null +++ b/IsoLib/t35_tool/extraction/SeiExtractor.cpp @@ -0,0 +1,417 @@ +#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..9ff029b8 --- /dev/null +++ b/IsoLib/t35_tool/injection/DedicatedIt35Strategy.cpp @@ -0,0 +1,333 @@ +#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..ed52dd06 --- /dev/null +++ b/IsoLib/t35_tool/injection/InjectionStrategy.cpp @@ -0,0 +1,27 @@ +#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..0df49460 --- /dev/null +++ b/IsoLib/t35_tool/injection/MebxMe4cStrategy.cpp @@ -0,0 +1,515 @@ +#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..41fc318b --- /dev/null +++ b/IsoLib/t35_tool/injection/SampleGroupStrategy.cpp @@ -0,0 +1,195 @@ +#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..7db7eee7 --- /dev/null +++ b/IsoLib/t35_tool/sources/GenericJsonSource.cpp @@ -0,0 +1,197 @@ +#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..b89c3cef --- /dev/null +++ b/IsoLib/t35_tool/sources/MetadataSource.cpp @@ -0,0 +1,36 @@ +#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..ceafc6d3 --- /dev/null +++ b/IsoLib/t35_tool/sources/SMPTEFolderSource.cpp @@ -0,0 +1,175 @@ +#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..9027e0a4 --- /dev/null +++ b/IsoLib/t35_tool/sources/SMPTE_ST2094_50.cpp @@ -0,0 +1,1210 @@ +#include "SMPTE_ST2094_50.hpp" + +#include "SMPTE_ST2094_50.hpp" +#include +#include +#include +#include + +/* *********************************** 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 ( + abs(cvt.hatm.gainApplicationSpaceChromaticities[0] - 0.64 ) < P_GAIN_APPLICATION_SPACE_CHROMATICITY && + abs(cvt.hatm.gainApplicationSpaceChromaticities[1] - 0.33 ) < P_GAIN_APPLICATION_SPACE_CHROMATICITY && + abs(cvt.hatm.gainApplicationSpaceChromaticities[2] - 0.30 ) < P_GAIN_APPLICATION_SPACE_CHROMATICITY && + abs(cvt.hatm.gainApplicationSpaceChromaticities[3] - 0.60 ) < P_GAIN_APPLICATION_SPACE_CHROMATICITY && + abs(cvt.hatm.gainApplicationSpaceChromaticities[4] - 0.15 ) < P_GAIN_APPLICATION_SPACE_CHROMATICITY && + abs(cvt.hatm.gainApplicationSpaceChromaticities[5] - 0.06 ) < P_GAIN_APPLICATION_SPACE_CHROMATICITY && + abs(cvt.hatm.gainApplicationSpaceChromaticities[6] - 0.3127) < P_GAIN_APPLICATION_SPACE_CHROMATICITY && + abs(cvt.hatm.gainApplicationSpaceChromaticities[7] - 0.3290) < P_GAIN_APPLICATION_SPACE_CHROMATICITY){ + elm.gain_application_space_chromaticities_mode = 0; + } + else if ( + abs(cvt.hatm.gainApplicationSpaceChromaticities[0] - 0.68 ) < P_GAIN_APPLICATION_SPACE_CHROMATICITY && + abs(cvt.hatm.gainApplicationSpaceChromaticities[1] - 0.32 ) < P_GAIN_APPLICATION_SPACE_CHROMATICITY && + abs(cvt.hatm.gainApplicationSpaceChromaticities[2] - 0.265 ) < P_GAIN_APPLICATION_SPACE_CHROMATICITY && + abs(cvt.hatm.gainApplicationSpaceChromaticities[3] - 0.69 ) < P_GAIN_APPLICATION_SPACE_CHROMATICITY && + abs(cvt.hatm.gainApplicationSpaceChromaticities[4] - 0.15 ) < P_GAIN_APPLICATION_SPACE_CHROMATICITY && + abs(cvt.hatm.gainApplicationSpaceChromaticities[5] - 0.06 ) < P_GAIN_APPLICATION_SPACE_CHROMATICITY && + abs(cvt.hatm.gainApplicationSpaceChromaticities[6] - 0.3127) < P_GAIN_APPLICATION_SPACE_CHROMATICITY && + abs(cvt.hatm.gainApplicationSpaceChromaticities[7] - 0.3290) < P_GAIN_APPLICATION_SPACE_CHROMATICITY ) { + elm.gain_application_space_chromaticities_mode = 1; + } else if ( + abs(cvt.hatm.gainApplicationSpaceChromaticities[0] - 0.708 ) < P_GAIN_APPLICATION_SPACE_CHROMATICITY && + abs(cvt.hatm.gainApplicationSpaceChromaticities[1] - 0.292 ) < P_GAIN_APPLICATION_SPACE_CHROMATICITY && + abs(cvt.hatm.gainApplicationSpaceChromaticities[2] - 0.17 ) < P_GAIN_APPLICATION_SPACE_CHROMATICITY && + abs(cvt.hatm.gainApplicationSpaceChromaticities[3] - 0.797 ) < P_GAIN_APPLICATION_SPACE_CHROMATICITY && + abs(cvt.hatm.gainApplicationSpaceChromaticities[4] - 0.131 ) < P_GAIN_APPLICATION_SPACE_CHROMATICITY && + abs(cvt.hatm.gainApplicationSpaceChromaticities[5] - 0.046 ) < P_GAIN_APPLICATION_SPACE_CHROMATICITY && + abs(cvt.hatm.gainApplicationSpaceChromaticities[6] - 0.3127) < P_GAIN_APPLICATION_SPACE_CHROMATICITY && + 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 ( + abs(cvt.hatm.cgf[iAlt].cm.componentMixRed ) < P_COMPONENT_MIXING_COEFFICIENT && + abs(cvt.hatm.cgf[iAlt].cm.componentMixGreen ) < P_COMPONENT_MIXING_COEFFICIENT && + abs(cvt.hatm.cgf[iAlt].cm.componentMixBlue ) < P_COMPONENT_MIXING_COEFFICIENT && + abs(cvt.hatm.cgf[iAlt].cm.componentMixMax- 1.0 ) < P_COMPONENT_MIXING_COEFFICIENT && + abs(cvt.hatm.cgf[iAlt].cm.componentMixMin ) < P_COMPONENT_MIXING_COEFFICIENT && + abs(cvt.hatm.cgf[iAlt].cm.componentMixComponent) < P_COMPONENT_MIXING_COEFFICIENT){ + elm.component_mixing_type[iAlt] = 0; + } else if ( + abs(cvt.hatm.cgf[iAlt].cm.componentMixRed ) < P_COMPONENT_MIXING_COEFFICIENT && + abs(cvt.hatm.cgf[iAlt].cm.componentMixGreen ) < P_COMPONENT_MIXING_COEFFICIENT && + abs(cvt.hatm.cgf[iAlt].cm.componentMixBlue ) < P_COMPONENT_MIXING_COEFFICIENT && + abs(cvt.hatm.cgf[iAlt].cm.componentMixMax ) < P_COMPONENT_MIXING_COEFFICIENT && + abs(cvt.hatm.cgf[iAlt].cm.componentMixMin ) < P_COMPONENT_MIXING_COEFFICIENT && + abs(cvt.hatm.cgf[iAlt].cm.componentMixComponent - 1.0 ) < P_COMPONENT_MIXING_COEFFICIENT){ + elm.component_mixing_type[iAlt] = 1; + } else if ( + abs(cvt.hatm.cgf[iAlt].cm.componentMixRed - (1.0 / 6.0)) < P_COMPONENT_MIXING_COEFFICIENT && + abs(cvt.hatm.cgf[iAlt].cm.componentMixGreen - (1.0 / 6.0)) < P_COMPONENT_MIXING_COEFFICIENT && + abs(cvt.hatm.cgf[iAlt].cm.componentMixBlue - (1.0 / 6.0)) < P_COMPONENT_MIXING_COEFFICIENT && + abs(cvt.hatm.cgf[iAlt].cm.componentMixMax - (1.0 / 2.0)) < P_COMPONENT_MIXING_COEFFICIENT && + abs(cvt.hatm.cgf[iAlt].cm.componentMixMin ) < P_COMPONENT_MIXING_COEFFICIENT && + 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.component_mixing_type = 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( 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..f51359ee --- /dev/null +++ b/IsoLib/t35_tool/t35_tool.cpp @@ -0,0 +1,307 @@ +/** + * @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); +}