// ------------------------------------------------------------------------------------------- // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // This file is part of the AWS CDI-SDK, licensed under the BSD 2-Clause "Simplified" License. // License details at: https://github.com/aws/aws-cdi-sdk/blob/mainline/LICENSE // ------------------------------------------------------------------------------------------- #include "riff.h" #include "cdi_avm_payloads_api.h" #include <assert.h> #include <ctype.h> #include <inttypes.h> #include "test_control.h" #include "test_common.h" //********************************************************************************************************************* //***************************************** START OF DEFINITIONS AND TYPES ******************************************** //********************************************************************************************************************* /// True if strings match, where rhs is a string literal or static string. #define STRINGS_MATCH(lhs, rhs) (0 == CdiOsStrNCmp((lhs), (rhs), strlen(rhs))) /// Define TestConsoleLog. #define TestConsoleLog SimpleConsoleLog /// Helper macro for calling CdiAvmUnpacketizeAncillaryData. #define MAKE_SGL(sgl, buffer_ptr, buffer_size) \ CdiSglEntry _entry = { \ .address_ptr = (void*)buffer_ptr, \ .size_in_bytes = buffer_size, \ .internal_data_ptr = NULL, \ .next_ptr = NULL \ }; \ const CdiSgList sgl = { \ .total_data_size = buffer_size, \ .sgl_head_ptr = &_entry, \ .sgl_tail_ptr = &_entry, \ .internal_data_ptr = NULL \ }; //********************************************************************************************************************* //******************************************* START OF STATIC FUNCTIONS *********************************************** //********************************************************************************************************************* /** * Check that file is a RIFF file. * * @param read_file_handle Handle to file. * @param file_path_str File path. * @param file_header_ptr Pointer to file header. * * @return True if and only if file is recognized as RIFF file. */ static bool IsRiffFile(CdiFileID read_file_handle, const char* file_path_str, RiffFileHeader* file_header_ptr) { uint32_t bytes_read = 0; bool return_val = CdiOsRead(read_file_handle, file_header_ptr, sizeof(RiffFileHeader), &bytes_read); if (!return_val || (sizeof(RiffFileHeader) != bytes_read)) { CDI_LOG_THREAD(kLogError, "Failed to read RIFF file header from file [%s].", file_path_str); return_val = false; } // Parse the header file. if (return_val) { // Check for "RIFF" four_cc marker. if (!STRINGS_MATCH(file_header_ptr->chunk_header.four_cc, "RIFF")) { CDI_LOG_THREAD(kLogError, "[%s] is not a RIFF file (four_cc code received is not 'RIFF').", file_path_str); return_val = false; } } return return_val; } /** * Return FourCC as a C string. * * @param four_cc Array holding a "four-character code". * * @return NUL-terminated static string of length four. */ static const char* FourCC(const char four_cc[4]) { static char four_cc_string[5] = { 0 }; four_cc_string[0] = four_cc[0]; four_cc_string[1] = four_cc[1]; four_cc_string[2] = four_cc[2]; four_cc_string[3] = four_cc[3]; return four_cc_string; } /** * Return whitespaces. * * @param indentation Number of space characters. * * @return NUL-terminated static string of length indentation. */ static const char* Space(int indentation) { static char space_string[128]; assert(indentation < ARRAY_ELEMENT_COUNT(space_string)); for (int i = 0; i < indentation; ++i) { space_string[i] = ' '; } space_string[indentation] = 0; return space_string; } /** * Write printable characters to buffer. * * @param indentation Number of whitespace characters to prefix each line with. * @param chunk_header A RIFF chunk header. * @param data_ptr Pointer to buffer with RIFF chunk data. * @param max_line_length Maximum number of characters to output per line. * @param print_buffer_ptr Pointer to print buffer. */ static void StringDumpChunk(int indentation, RiffChunkHeader chunk_header, const char* data_ptr, int max_line_length, char* print_buffer_ptr) { int i = 0; for (; i < indentation; ++i) { print_buffer_ptr[i] = ' '; } i += snprintf(print_buffer_ptr + i, max_line_length - i, "%s (%4"PRIu32"): ", FourCC(chunk_header.four_cc), chunk_header.size); uint32_t j = 0; while (i < max_line_length && j < chunk_header.size) { print_buffer_ptr[i++] = isprint((unsigned char)data_ptr[j]) ? data_ptr[j] : '.'; j++; } // Indicate whether there is more data than we can print on one line. if (i < max_line_length) { print_buffer_ptr[i] = '<'; } else { assert(i == max_line_length); print_buffer_ptr[i] = '>'; } print_buffer_ptr[max_line_length + 1] = 0; } /// Control structure for Anc unpacketize callback. struct UnpacketizeAncControl { char* print_buffer_ptr; ///< Pointer to print buffer. int max_line_length; ///< Maximum number of characters to print. }; /** * Callback used by UnpacketizeAncPayload. Writes user data into results buffer. * * @see CdiAvmUnpacketizeAncCallback. */ static void ShowAncCallback(void* context_ptr, CdiFieldKind field_kind, const CdiAvmAncillaryDataPacket* packet_ptr, bool has_data_count_parity_error, bool has_checksum_error) { struct UnpacketizeAncControl* ctrl_ptr = (struct UnpacketizeAncControl*)context_ptr; if (NULL != packet_ptr && 0 < ctrl_ptr->max_line_length) { int i = snprintf(ctrl_ptr->print_buffer_ptr, ctrl_ptr->max_line_length, "DID/SDID/UDWs: 0x%x/0x%x/%u, ", packet_ptr->did, packet_ptr->sdid, packet_ptr->data_count); ctrl_ptr->max_line_length -= i; ctrl_ptr->print_buffer_ptr += i; } if (NULL == packet_ptr && has_data_count_parity_error) { int i = snprintf(ctrl_ptr->print_buffer_ptr, ctrl_ptr->max_line_length, "!PARITY ERROR "); ctrl_ptr->max_line_length -= i; ctrl_ptr->print_buffer_ptr += i; } if (NULL == packet_ptr && has_checksum_error) { int i = snprintf(ctrl_ptr->print_buffer_ptr, ctrl_ptr->max_line_length, "!CHECKSUM ERROR"); ctrl_ptr->max_line_length -= i; ctrl_ptr->print_buffer_ptr += i; } (void)field_kind; // unused (void)has_data_count_parity_error; // unused (void)has_checksum_error; // unused } /** * Helper for extracting CEA-608 encoded closed captions: translate character code. * * @param cc Character code to translate. * * @return ASCII character code. */ static char Translate608(uint8_t cc) { const char table[] = { ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '!', '"', '#', '$', '%', '&', '\'', '(', ')', 'a', '+', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', 'e', ']', 'i', 'o', 'u', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'c', '%', 'N', 'n', '+' }; assert(cc < ARRAY_ELEMENT_COUNT(table)); // cc is assumed a standard characters per ANSI/CTA-608-E S-2019, Table 50. return table[cc]; } /** * Callback used by UnpacketizeAncPayload. Writes user data into results buffer. * * @see CdiAvmUnpacketizeAncCallback. */ static void ShowCCsCallback(void* context_ptr, CdiFieldKind field_kind, const CdiAvmAncillaryDataPacket* packet_ptr, bool has_data_count_parity_error, bool has_checksum_error) { struct UnpacketizeAncControl* ctrl_ptr = (struct UnpacketizeAncControl*)context_ptr; if (NULL != packet_ptr && 0 < ctrl_ptr->max_line_length) { if (0x61 == packet_ptr->did) { if (0x02 == packet_ptr->sdid) { // CEA-608 data (see SMPTE ST 334-1:2015, Table 1). assert(3 == packet_ptr->data_count); bool is_field1 = packet_ptr->user_data[0] & 0x80; uint8_t cc1 = packet_ptr->user_data[1] & 0x7f; uint8_t cc2 = packet_ptr->user_data[2] & 0x7f; if (is_field1 && 0x19 < cc1 && cc1 < 0x80) { // Standard characters. *ctrl_ptr->print_buffer_ptr++ = Translate608(cc1); *ctrl_ptr->print_buffer_ptr++ = Translate608(cc2); ctrl_ptr->max_line_length -= 2; } } if (0x01 == packet_ptr->sdid) { // TODO: CEA-708 Closed captioning assert(0); // Need implementation here. } } } (void)field_kind; // unused (void)has_data_count_parity_error; // unused (void)has_checksum_error; // unused } /** * Write printable characters to buffer. * * @param indentation Number of whitespace characters to prefix each line with. * @param chunk_header A RIFF chunk header. * @param data_ptr Pointer to buffer with RIFF chunk data. * @param max_line_length Maximum number of characters to output per line. * @param print_buffer_ptr Pointer to print buffer. * @param mode Dump mode. * * @return Success or failure. */ static bool ShowAncPayload(int indentation, RiffChunkHeader chunk_header, const char* data_ptr, int max_line_length, char* print_buffer_ptr, int mode) { // Indent unless it's CC dump mode. CdiAvmUnpacketizeAncCallback* callback = ShowAncCallback; int i = 0; if (kRiffDumpClosedCaptions == mode) { callback = ShowCCsCallback; i = strlen(print_buffer_ptr); } else { for (; i < indentation; ++i) { print_buffer_ptr[i] = ' '; } } struct UnpacketizeAncControl ctrl = { .print_buffer_ptr = print_buffer_ptr + i, .max_line_length = max_line_length - i }; MAKE_SGL(sgl, data_ptr, chunk_header.size); CdiReturnStatus rs = CdiAvmUnpacketizeAncillaryData(&sgl, callback, &ctrl); if (kCdiStatusOk != rs) { CDI_LOG_THREAD(kLogError, "Error processing ANC payload [%s].", CdiCoreStatusToString(rs)); // Fall back on StringDumpChunk. StringDumpChunk(indentation, chunk_header, data_ptr, max_line_length, print_buffer_ptr); } return kCdiStatusOk == rs; } /** * Check that chunk data is ancillary data. * * @param data_ptr Pointer to chunk data. * @param data_size Size in bytes. * * @return True if and only if data is decodable as ancillary data. */ static bool CheckAncPayload(const char* data_ptr, uint32_t data_size) { // We don't want to print anything here, hence max_line_length = 0. char print_buffer[1]; struct UnpacketizeAncControl ctrl = { .print_buffer_ptr = print_buffer, .max_line_length = 0 }; MAKE_SGL(sgl, data_ptr, data_size); CdiReturnStatus rs = CdiAvmUnpacketizeAncillaryData(&sgl, ShowAncCallback, &ctrl); return kCdiStatusOk == rs; } /** * Show RIFF data by sub chunk. * * @param file_handle Handle to RIFF file. * @param size Size in bytes of list of sub chunks. * @param indentation Number of spaces to indent output by. * @param max_line_length Number of spaces per line to output. * @param mode Dump mode selecting the kind of data to show. * * @return Success or failure. */ static bool ShowRiffList(CdiFileID file_handle, uint32_t size, int indentation, int max_line_length, int mode) { char print_buffer[256] = { 0 }; bool success = true; uint32_t list_bytes_read = 0; while (success && list_bytes_read < size) { uint32_t bytes_read = 0; RiffChunkHeader chunk_header; success = CdiOsRead(file_handle, &chunk_header, sizeof(RiffChunkHeader), &bytes_read); if (!success || (sizeof(RiffChunkHeader) != bytes_read)) { TestConsoleLog(kLogError, "Failed to read chunk header."); TestConsoleLog(kLogError, "list_bytes_read = [%u], size = [%u]", list_bytes_read, size); break; } list_bytes_read += bytes_read; // Show this chunk. if (STRINGS_MATCH(chunk_header.four_cc, "LIST")) { char four_cc[4]; success = CdiOsRead(file_handle, four_cc, 4, &bytes_read); if (!success || (4 != bytes_read)) { TestConsoleLog(kLogError, "Failed to read form type."); break; } list_bytes_read += bytes_read; TestConsoleLog(kLogInfo, "%s%s (%"PRIu32" bytes):", Space(indentation), FourCC(four_cc), chunk_header.size); ShowRiffList(file_handle, chunk_header.size - 4, indentation + 2, max_line_length, mode); } else { char* p = CdiOsMemAlloc(chunk_header.size); assert(NULL != p); success = CdiOsRead(file_handle, p, chunk_header.size, &bytes_read); if (!success || (chunk_header.size != bytes_read)) { CdiOsMemFree(p); TestConsoleLog(kLogError, "Failed to read data for [%s] chunk (%"PRIu32" vs %"PRIu32" expected).", FourCC(chunk_header.four_cc), bytes_read, chunk_header.size); break; } assert(max_line_length < ARRAY_ELEMENT_COUNT(print_buffer)); switch (mode) { case kRiffDumpRaw: StringDumpChunk(indentation, chunk_header, p, max_line_length, print_buffer); break; case kRiffDumpDid: case kRiffDumpClosedCaptions: if (STRINGS_MATCH(chunk_header.four_cc, "ANC ")) { if (0 != bytes_read % 4) { TestConsoleLog(kLogWarning, "Invalid ANC chunk size [%u].", bytes_read); } } success = ShowAncPayload(indentation, chunk_header, p, max_line_length, print_buffer, mode); break; default: assert(0); } CdiOsMemFree(p); // When extracting closed captions, don't print every chunk. // When printing DID/SDID, don't print empty lines for empty ANC packets. bool print_now = (kRiffDumpClosedCaptions == mode && (int)strlen(print_buffer) >= max_line_length) || (kRiffDumpDid == mode && (int)strlen(print_buffer) > indentation) || (kRiffDumpRaw == mode); if (print_now) { TestConsoleLog(kLogInfo, "%s", print_buffer); memset(print_buffer, 0, ARRAY_ELEMENT_COUNT(print_buffer)); } } list_bytes_read += chunk_header.size; } if (success && strlen(print_buffer) > 0) { TestConsoleLog(kLogInfo, "%s", print_buffer); } return success; } /** * Check that a RIFF file contains ancillary data. * * @param file_handle Handle to RIFF file. * @param size Size in bytes of list of sub chunks. * @param verbose When true, log error messages explaining what's wrong with the file. * * @return True if and only if file contains ancillary data. */ static bool CheckFileContainsAncData(CdiFileID file_handle, uint32_t size, bool verbose) { uint32_t list_bytes_read = 0; bool success = true; while (success && list_bytes_read < size) { uint32_t bytes_read = 0; RiffChunkHeader chunk_header; success = CdiOsRead(file_handle, &chunk_header, sizeof(RiffChunkHeader), &bytes_read); if (!success || (sizeof(RiffChunkHeader) != bytes_read)) { if (verbose) { TestConsoleLog(kLogError, "Failed to read chunk header."); TestConsoleLog(kLogError, "list_bytes_read = [%u], size = [%u]", list_bytes_read, size); } continue; } list_bytes_read += bytes_read; char* p = CdiOsMemAlloc(chunk_header.size); assert(NULL != p); success = CdiOsRead(file_handle, p, chunk_header.size, &bytes_read); if (!success || (chunk_header.size != bytes_read)) { if (verbose) { TestConsoleLog(kLogError, "Failed to read data for [%s] chunk (%"PRIu32" vs %"PRIu32" expected).", FourCC(chunk_header.four_cc), bytes_read, chunk_header.size); } success = false; } if (success && STRINGS_MATCH(chunk_header.four_cc, "ANC ")) { if (0 != bytes_read % 4) { if (verbose) { TestConsoleLog(kLogError, "Expected multiple of four as ANC chunk size, got [%u].", bytes_read); } success = false; } else { success = CheckAncPayload(p, chunk_header.size); } } else { if (verbose) { TestConsoleLog(kLogWarning, "Expected ANC chunk, got [%s].", FourCC(chunk_header.four_cc)); } success = false; } CdiOsMemFree(p); list_bytes_read += chunk_header.size; } return success; } //********************************************************************************************************************* //******************************************* START OF PUBLIC FUNCTIONS *********************************************** //********************************************************************************************************************* bool StartRiffPayloadFile(const StreamSettings* stream_settings_ptr, CdiFileID read_file_handle) { RiffFileHeader file_header; bool return_val = IsRiffFile(read_file_handle, stream_settings_ptr->file_read_str, &file_header); if (return_val) { // Check for "CDI " Form Type. if (!STRINGS_MATCH(file_header.form_type, "CDI ")) { CDI_LOG_THREAD(kLogError, "RIFF file [%s]: Form Type received is not 'CDI '.", stream_settings_ptr->file_read_str, file_header.form_type); return_val = false; } } return return_val; } bool GetNextRiffChunkSize(const StreamSettings* stream_settings_ptr, CdiFileID read_file_handle, int* ret_chunk_size_ptr) { bool return_val = true; RiffChunkHeader chunk_header; // Buffer for holding chunk headers four_cc code and chunk size. uint32_t bytes_read = 0; if (read_file_handle) { return_val = CdiOsRead(read_file_handle, &chunk_header, sizeof(RiffChunkHeader), &bytes_read); } else { CDI_LOG_THREAD(kLogError, "No file handle for RIFF File"); } // Ran out of subchunk headers to read so retry at the top of the file. if (return_val && (0 == bytes_read)) { if (CdiOsFSeek(read_file_handle, 0, SEEK_SET)) { return_val = StartRiffPayloadFile(stream_settings_ptr, read_file_handle); } if (return_val) { return_val = CdiOsRead(read_file_handle, &chunk_header, sizeof(RiffChunkHeader), &bytes_read); } } if (!return_val || (sizeof(RiffChunkHeader) != bytes_read)) { CDI_LOG_THREAD(kLogError, "Failed to read chunk header from file [%s]. Read [%d] header bytes.", stream_settings_ptr->file_read_str, bytes_read); return_val = false; } // For now check if the chunk ID is "ANC ". NOTE: this check may be removed or expanded in the future to support // additional chunk IDs. if (!STRINGS_MATCH(chunk_header.four_cc, "ANC ")) { CDI_LOG_THREAD(kLogError, "RIFF File [%s] subchunk ID is not 'ANC '.", stream_settings_ptr->file_read_str); return_val = false; } if (return_val) { *ret_chunk_size_ptr = chunk_header.size; // Payload size must be larger than the RIFF chunk size in the source file. if (*ret_chunk_size_ptr > stream_settings_ptr->payload_size) { CDI_LOG_THREAD(kLogError, "Payload size from RIFF file [%d] is larger than the payload buffer [%d].", *ret_chunk_size_ptr, stream_settings_ptr->payload_size); return_val = false; } } return return_val; } bool ReportRiffFileContents(const char* file_path_str, int max_line_length, int mode) { if (kRiffDumpNone == mode) { return false; } CdiFileID file_handle; if (!CdiOsOpenForRead(file_path_str, &file_handle)) { return false; } RiffFileHeader file_header; bool success = IsRiffFile(file_handle, file_path_str, &file_header); if (success) { // Print the contents. if (kRiffDumpClosedCaptions != mode) { TestConsoleLog(kLogInfo, ""); TestConsoleLog(kLogInfo, "%4s (%"PRIu32" bytes):", FourCC(file_header.form_type), file_header.chunk_header.size); } success = ShowRiffList(file_handle, file_header.chunk_header.size - 4, 2, max_line_length, mode); } CdiOsClose(file_handle); return success; } bool RiffFileContainsAncillaryData(const char* file_path_str) { CdiFileID file_handle; if (!CdiOsOpenForRead(file_path_str, &file_handle)) { return false; } RiffFileHeader file_header; if (!IsRiffFile(file_handle, file_path_str, &file_header)) { return false; } if (!STRINGS_MATCH(file_header.form_type, "CDI ")) { return false; } return CheckFileContainsAncData(file_handle, file_header.chunk_header.size - 4, true); }